11、运算符重载:让对象支持加减乘除

运算符重载,说白了就是让自定义的对象也能用上 +-*/ 这些符号。我刚开始学 Python 时觉得这功能挺花哨的,直到自己写了一个复数类,才发现没有运算符重载,代码会变得多别扭。

你想想看,如果两个复数相加,你希望写成 c1 + c2 还是 c1.add(c2)?前者多直观啊。Python 之所以能这么做,靠的就是 __add____sub__ 这些魔法方法。

11.1 什么是运算符重载?

运算符重载,就是让同一个运算符(比如 +)在不同类型上表现出不同的行为。Python 内部其实把 a + b 解释成了 a.__add__(b)。你只要在类里定义好 __add__ 方法,你的对象就能用 + 了。

我个人习惯把运算符重载分成三类:

  • 算术运算符+-*///%**
  • 比较运算符==!=<><=>=
  • 赋值运算符+=-=*=

对应的魔法方法也很规律:

运算符 魔法方法 说明
+ __add__(self, other) 加法
- __sub__(self, other) 减法
* __mul__(self, other) 乘法
/ __truediv__(self, other) 除法
// __floordiv__(self, other) 整除
% __mod__(self, other) 取模
** __pow__(self, other) 幂运算
== __eq__(self, other) 等于
!= __ne__(self, other) 不等于
< __lt__(self, other) 小于
+= __iadd__(self, other) 自增
小技巧: 如果你实现了 __eq__,Python 会自动帮你处理 __ne__(不等于),但前提是你没手动定义 __ne__。我建议还是两个都写上,避免某些边界情况出问题。

11.2 实战案例:复数类

复数是个很经典的例子。数学上复数有实部和虚部,加减乘除都有明确的规则。我们来手写一个 Complex 类。

class Complex:
    def __init__(self, real, imag):
        self.real = real
        self.imag = imag

    def __add__(self, other):
        """加法:实部相加,虚部相加"""
        return Complex(self.real + other.real, self.imag + other.imag)

    def __sub__(self, other):
        """减法:实部相减,虚部相减"""
        return Complex(self.real - other.real, self.imag - other.imag)

    def __mul__(self, other):
        """乘法:(a+bi)(c+di) = (ac-bd) + (ad+bc)i"""
        real = self.real * other.real - self.imag * other.imag
        imag = self.real * other.imag + self.imag * other.real
        return Complex(real, imag)

    def __truediv__(self, other):
        """除法:用共轭复数化简"""
        denom = other.real ** 2 + other.imag ** 2
        real = (self.real * other.real + self.imag * other.imag) / denom
        imag = (self.imag * other.real - self.real * other.imag) / denom
        return Complex(real, imag)

    def __eq__(self, other):
        """判断两个复数是否相等"""
        return self.real == other.real and self.imag == other.imag

    def __repr__(self):
        """友好的显示方式"""
        if self.imag >= 0:
            return f"{self.real} + {self.imag}i"
        else:
            return f"{self.real} - {abs(self.imag)}i"

写完之后,我们来试试效果:

c1 = Complex(3, 4)   # 3 + 4i
c2 = Complex(1, -2)  # 1 - 2i

print(c1 + c2)  # 4 + 2i
print(c1 - c2)  # 2 + 6i
print(c1 * c2)  # 11 - 2i
print(c1 / c2)  # -1.0 + 2.0i
print(c1 == Complex(3, 4))  # True

你看,代码读起来就像在写数学公式一样自然。这就是运算符重载的魅力。

核心思想: 运算符重载不是为了炫技,而是为了让你的类用起来像内置类型一样自然。如果你的类有「相加」「比较」等语义,那就应该重载对应的运算符。

11.3 反向运算符与复合赋值

有时候你会遇到 2 + c1 这种写法。Python 会先尝试 int.__add__(2, c1),但整数不认识复数,就会报错。这时候就需要反向运算符了。

反向运算符的魔法方法名是 __radd____rsub__ 等。当左操作数不支持运算时,Python 会调用右操作数的反向方法。

class Complex:
    # ... 前面的代码不变 ...

    def __radd__(self, other):
        """处理 other + self 的情况"""
        return Complex(other + self.real, self.imag)

    def __rsub__(self, other):
        """处理 other - self 的情况"""
        return Complex(other - self.real, -self.imag)

    def __rmul__(self, other):
        """处理 other * self 的情况"""
        return Complex(other * self.real, other * self.imag)

复合赋值运算符(+=-= 等)也有对应的方法:

class Complex:
    def __iadd__(self, other):
        """自增:c1 += c2"""
        self.real += other.real
        self.imag += other.imag
        return self

    def __isub__(self, other):
        """自减:c1 -= c2"""
        self.real -= other.real
        self.imag -= other.imag
        return self
注意: 如果你没有定义 __iadd__,Python 会退而求其次,用 __add__ 创建一个新对象再赋值。这会导致性能损失,尤其是处理大量数据时。我曾经在一个科学计算项目里踩过这个坑,循环里用了 += 结果内存暴涨,排查了半天才发现是没重载 __iadd__

11.4 运算符重载的避坑指南

运算符重载虽然好用,但也不是越多越好。我总结了几条经验:

  • 不要改变运算符的语义+ 就应该表示「相加」,别搞成「相减」或者「拼接」,否则别人看你的代码会一头雾水。
  • 保持对称性:如果实现了 __eq__,最好也实现 __ne__。如果实现了 __lt__,最好也实现 __gt____le____ge__
  • 考虑类型检查:在方法里可以加个 isinstance 判断,避免传入不兼容的类型导致崩溃。
  • 不要滥用:如果你的类没有明确的「运算」语义,就别硬重载。比如一个 User 类,两个用户相加是什么意思?说不清楚就别做。
def __add__(self, other):
    if not isinstance(other, Complex):
        raise TypeError(f"不支持 Complex 和 {type(other).__name__} 相加")
    return Complex(self.real + other.real, self.imag + other.imag)

11.5 知识体系图

下面这张图帮你理清运算符重载的整体脉络:

运算符重载 算术运算符 比较运算符 复合赋值 __add__ __sub__ __mul__ __eq__ __lt__ __gt__ __iadd__ __isub__ __imul__ 反向运算符 __radd__ __rsub__ __rmul__ 实战案例:复数类 加减乘除 + 比较 + 反向运算 避坑指南 不改变语义 · 保持对称 · 类型检查

11.6 总结

运算符重载是 Python 面向对象编程中非常实用的一环。它让自定义类用起来像内置类型一样自然,代码可读性大大提升。

我个人觉得,判断要不要重载运算符的标准很简单:如果这个操作在数学或业务上有明确的含义,那就重载;如果只是为了省事而强行赋予意义,那就别做。

嗯,最后提醒一句:别忘了实现 __repr____str__,否则你打印对象时看到的会是 <__main__.Complex object at 0x...>,调试起来很痛苦。我曾经因为这个浪费了半小时,还以为代码写错了。

练习建议: 试着给复数类加上 __pow__ 方法,实现复数的幂运算。提示:可以用欧拉公式 r * e^(iθ) 来简化计算。

公众号:蓝海资料掘金营,微信 deep3321