3. 算术操作符重载(上):__add__、__sub__、__mul__、__truediv__、__floordiv__

算术操作符重载,说白了就是让自定义对象也能用 +、-、*、/ 这些符号。Python 默认只认识数字和字符串怎么相加,换成你的对象它就懵了。我们得告诉它:「嘿,这两个对象相加,应该这么算。」

我个人习惯把操作符重载看作「给类安装数学基因」。你想想看,如果设计一个 Vector2D 类,每次相加都要写 v1.add(v2) 多别扭?直接用 v1 + v2 才符合直觉。

核心原则:操作符重载不是炫技,而是让代码更自然、更可读。能用 + 表达的含义,就别用方法名硬凑。

3.1 五虎上将:算术操作符家族

Python 为每个算术操作符都预留了「双下划线方法」。我们这章先搞定最常用的五个:

操作符 方法名 数学含义 典型场景
+ __add__ 加法 向量相加、金额合并
- __sub__ 减法 坐标偏移、库存扣减
* __mul__ 乘法 标量缩放、数量×单价
/ __truediv__ 真除法 平均分配、精确计算
// __floordiv__ 整除 分页、分组、取整

嗯,这里要注意:__truediv__ 和 __floordiv__ 是 Python 3 才严格区分的。Python 2 里 / 默认是整除,踩过坑的老程序员都懂。我刚开始用 Python 3 时,还习惯性地写 from __future__ import division,后来发现完全没必要了。

3.2 实战:构建一个 Money 类

光讲理论没意思,我们直接写一个 Money 类。它代表金额,包含币种和数值。两个 Money 对象相加,币种必须一致,否则报错。

class Money:
    def __init__(self, amount: float, currency: str = "CNY"):
        self.amount = amount
        self.currency = currency

    def __add__(self, other):
        """实现 Money + Money 或 Money + 数字"""
        if isinstance(other, Money):
            if self.currency != other.currency:
                raise ValueError(f"币种不一致:{self.currency} vs {other.currency}")
            return Money(self.amount + other.amount, self.currency)
        elif isinstance(other, (int, float)):
            return Money(self.amount + other, self.currency)
        return NotImplemented

    def __sub__(self, other):
        """实现 Money - Money 或 Money - 数字"""
        if isinstance(other, Money):
            if self.currency != other.currency:
                raise ValueError(f"币种不一致:{self.currency} vs {other.currency}")
            return Money(self.amount - other.amount, self.currency)
        elif isinstance(other, (int, float)):
            return Money(self.amount - other, self.currency)
        return NotImplemented

    def __mul__(self, other):
        """实现 Money * 数字(标量乘法)"""
        if isinstance(other, (int, float)):
            return Money(self.amount * other, self.currency)
        return NotImplemented

    def __truediv__(self, other):
        """实现 Money / 数字(精确除法)"""
        if isinstance(other, (int, float)):
            return Money(self.amount / other, self.currency)
        return NotImplemented

    def __floordiv__(self, other):
        """实现 Money // 数字(整除)"""
        if isinstance(other, (int, float)):
            return Money(self.amount // other, self.currency)
        return NotImplemented

    def __repr__(self):
        return f"Money({self.amount:.2f}, '{self.currency}')"
小技巧:当遇到不支持的类型时,返回 NotImplemented 而不是抛出 TypeError。这样 Python 会给对方类型一个「反向操作」的机会,更符合鸭子类型哲学。

来测试一下:

salary = Money(15000.0, "CNY")
bonus = Money(3000.0, "CNY")
total = salary + bonus
print(total)  # Money(18000.00, 'CNY')

tax_rate = 0.15
after_tax = salary * (1 - tax_rate)
print(after_tax)  # Money(12750.00, 'CNY')

per_person = total // 3
print(per_person)  # Money(6000.00, 'CNY')

你看,代码读起来就像在操作普通数字一样自然。我在项目中遇到过类似场景——处理多币种订单时,用这种类封装金额,比到处传 (amount, currency) 元组安全得多。

3.3 避坑指南:类型检查与边界情况

操作符重载最容易翻车的地方,就是类型处理。我曾经在线上环境踩过一个坑:

我曾经踩过的坑:某次写金融计算模块,__add__ 里只处理了 Money + Money,没考虑 Money + int。结果某天业务方传了个 salary + 5000,直接报 TypeError,线上告警炸了。从那以后,我所有算术方法都加上了 isinstance 分支。

写算术操作符时,建议遵循以下检查顺序:

  1. 先判断类型:用 isinstance 区分同类运算和混合运算
  2. 再校验数据:币种、单位、量纲是否一致
  3. 最后执行计算:返回新对象,不要修改原对象
  4. 兜底返回 NotImplemented:让 Python 尝试反向操作

还有一个容易忽略的点:不可变性。算术操作应该返回新对象,而不是修改 self。你想想看,a + b 如果偷偷把 a 改了,调试起来得多崩溃?

3.4 知识体系:操作符重载的调用链路

为了让你更直观地理解 Python 内部是怎么处理 a + b 的,我画了张流程图:

Python 操作符重载调用链路 a + b a 有 __add__? 是 调用 a.__add__(b) 返回结果 否 尝试 b.__radd__(a) 有 __radd__? 是 调用 b.__radd__(a) 抛出 TypeError 💡 关键点: • 先尝试左操作数的正向方法(__add__) • 如果返回 NotImplemented,再尝试右操作数的反向方法(__radd__) • 两边都不支持,才抛出 TypeError

这个流程解释了为什么我们返回 NotImplemented 而不是直接抛异常。Python 的「反向操作」机制(比如 __radd__)给了右操作数一个补救机会。虽然这章我们没讲反向方法,但了解这个链路能帮你理解操作符重载的全貌。

3.5 实战对比:不同返回类型的取舍

写算术方法时,返回类型怎么定?我见过三种做法:

策略 示例 优点 缺点
返回同类对象 Money + Money → Money 链式调用方便,类型一致 需要处理类型转换
返回基础类型 Money + Money → float 简单直接 丢失上下文(币种信息)
返回新类型 Money * int → Money 语义清晰 类型系统变复杂

我个人推荐「返回同类对象」。你想想看,salary + bonus 的结果应该还是个 Money,这样才能继续参与后续运算。如果返回 float,币种信息就丢了,再跟其他 Money 相加时还得重新包装。

设计建议:算术方法返回新对象时,尽量保持类型一致。如果运算结果语义上「降级」了(比如除法可能产生小数),可以考虑返回基础类型,但要做好文档说明。

3.6 本章小结

算术操作符重载,说白了就是让自定义类「学会」数学运算。我们这章搞定了五个最常用的方法:

  • __add__ 和 __sub__:同类对象相加/相减,注意类型检查和数据校验
  • __mul__:标量乘法,常用于缩放
  • __truediv__ 和 __floordiv__:精确除法和整除,场景不同要区分
  • 返回 NotImplemented 而不是抛异常,给反向操作留机会
  • 保持不可变性,每次运算返回新对象