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 知识体系图
下面这张图帮你理清运算符重载的整体脉络:
11.6 总结
运算符重载是 Python 面向对象编程中非常实用的一环。它让自定义类用起来像内置类型一样自然,代码可读性大大提升。
我个人觉得,判断要不要重载运算符的标准很简单:如果这个操作在数学或业务上有明确的含义,那就重载;如果只是为了省事而强行赋予意义,那就别做。
嗯,最后提醒一句:别忘了实现 __repr__ 或 __str__,否则你打印对象时看到的会是 <__main__.Complex object at 0x...>,调试起来很痛苦。我曾经因为这个浪费了半小时,还以为代码写错了。
__pow__ 方法,实现复数的幂运算。提示:可以用欧拉公式 r * e^(iθ) 来简化计算。
公众号:蓝海资料掘金营,微信 deep3321