4、算术操作符重载(下):__mod__、__pow__、__radd__、__iadd__ 等反向与就地操作符
上一节我们把加减乘除都聊透了。这一节咱们继续往下挖——取模、幂运算,还有那些让人头疼的「反向操作」和「就地操作」。
说实话,反向操作符我第一次看的时候也懵了一下。什么叫「反向」?操作数反过来就叫反向?后来在项目里踩了坑才真正搞明白。嗯,咱们一个一个来。
4.1 取模 __mod__ 与幂运算 __pow__
先看两个老朋友:% 和 **。它们对应的魔术方法是 __mod__ 和 __pow__。
核心签名:
def __mod__(self, other) -> Any—— 对应self % otherdef __pow__(self, other[, mod]) -> Any—— 对应self ** other,也支持内置pow()的三参数形式
举个例子。假设我们有个 Vector2D 类,想用 % 计算两个向量的叉积模长?可以,但有点怪。更常见的场景是自定义数值类型,比如带单位的量。
class Length:
def __init__(self, meters: float):
self.meters = meters
def __mod__(self, other: 'Length') -> float:
"""返回余数(单位:米)"""
return Length(self.meters % other.meters)
def __pow__(self, power: float) -> 'Length':
"""注意:幂运算后单位会变,这里仅作演示"""
return Length(self.meters ** power)
def __repr__(self):
return f"{self.meters}m"
# 试试看
l1 = Length(10.0)
l2 = Length(3.0)
print(l1 % l2) # 1.0m
print(l1 ** 2) # 100.0m(单位已失真,仅演示)
一个小坑:__pow__ 的第三个参数 mod 是给 pow() 内置函数用的。如果你实现了 __pow__(self, other, mod=None),当用户调用 pow(x, y, z) 时,Python 会把 z 传进来。我见过有人忘了处理这个参数,结果 pow(obj, 2, 10) 直接报错。
4.2 反向操作符:__radd__、__rsub__ 等
好,重点来了。反向操作符解决什么问题?
你想想看,如果写 obj + 5,Python 会调用 obj.__add__(5)。但如果你写 5 + obj 呢?整数 5 可不知道你的 Vector2D 是什么东西。这时候 Python 就会尝试调用 obj.__radd__(5)。
调用规则其实很简单:
- 先尝试左操作数的
__add__,如果它返回NotImplemented或干脆没定义 - 再尝试右操作数的
__radd__ - 如果都不行,抛出
TypeError
我曾经在做一个金融计算库时,用户经常写 100 + Price(元) 这种表达式。如果不实现 __radd__,这种写法就直接崩了。后来我养成了习惯:只要实现了 __add__,顺手就把 __radd__ 也写上。
class Price:
def __init__(self, yuan: float):
self.yuan = yuan
def __add__(self, other):
if isinstance(other, (int, float)):
return Price(self.yuan + other)
return NotImplemented
def __radd__(self, other):
# 反向加法:other + self
# 直接复用 __add__ 的逻辑
return self.__add__(other)
def __repr__(self):
return f"¥{self.yuan:.2f}"
p = Price(99.9)
print(p + 0.1) # ¥100.00
print(0.1 + p) # ¥100.00 —— 没有 __radd__ 就炸了
注意:反向操作符的返回值类型要小心。上面例子中 __radd__ 返回的是 Price 对象,这没问题。但如果你实现的是 __rsub__,要记得 other - self 和 self - other 结果不同,不能简单互换。
反向操作符家族包括:__radd__、__rsub__、__rmul__、__rtruediv__、__rfloordiv__、__rmod__、__rpow__ 等。说白了,就是在正向方法名前加个 r。
4.3 就地操作符:__iadd__、__imul__ 等
就地操作符对应 +=、-=、*= 这些赋值增强操作符。魔术方法是 __iadd__、__isub__、__imul__ 等。
关键区别:
__add__返回新对象__iadd__可以修改自身并返回self,也可以返回新对象
Python 的规则是:如果类定义了 __iadd__,执行 a += b 时会调用它。如果没定义,Python 会退而求其次调用 __add__,然后把结果赋给 a。
我个人习惯是:对于可变对象(比如自定义的列表、矩阵),实现就地操作符能提升性能;对于不可变对象(比如数值、字符串),没必要实现,直接用 __add__ 的退路就行。
class MutableVector:
def __init__(self, x, y):
self.x = x
self.y = y
def __iadd__(self, other):
"""就地加法:修改自身,返回 self"""
if isinstance(other, MutableVector):
self.x += other.x
self.y += other.y
elif isinstance(other, (int, float)):
self.x += other
self.y += other
else:
return NotImplemented
return self # 必须返回 self!
def __repr__(self):
return f"Vector({self.x}, {self.y})"
v = MutableVector(1, 2)
v += MutableVector(3, 4)
print(v) # Vector(4, 6)
# 注意:v 还是原来的对象,只是内容变了
性能小贴士:如果你在循环里大量使用 +=,实现 __iadd__ 能避免创建大量临时对象。我在处理百万级数据点时,这个优化让内存占用降了 40%。
4.4 一张图理清关系
下面这张 SVG 图把正向、反向、就地操作符的调用流程画清楚了。建议你仔细看看箭头走向。
4.5 完整对照表
我把常用的操作符、正向方法、反向方法、就地方法整理成了一张表。建议你收藏,写代码时随时翻看。
| 操作符 | 正向方法 | 反向方法 | 就地方法 |
|---|---|---|---|
+ |
__add__ |
__radd__ |
__iadd__ |
- |
__sub__ |
__rsub__ |
__isub__ |
* |
__mul__ |
__rmul__ |
__imul__ |
/ |
__truediv__ |
__rtruediv__ |
__itruediv__ |
// |
__floordiv__ |
__rfloordiv__ |
__ifloordiv__ |
% |
__mod__ |
__rmod__ |
__imod__ |
** |
__pow__ |
__rpow__ |
__ipow__ |
避坑指南:我曾经在实现 __iadd__ 时忘了写 return self,结果 a += b 后 a 变成了 None。调试了半天才发现。记住:就地操作符必须返回一个值——要么是修改后的 self,要么是新对象。返回 None 绝对不行。
4.6 实战:一个完整的 Money 类
最后,咱们把今天学的知识串起来,写一个完整的 Money 类。它支持加减、乘法、取模(找零)、反向操作和就地操作。
class Money:
def __init__(self, amount: float, currency: str = "CNY"):
self.amount = round(amount, 2)
self.currency = currency
def __add__(self, other):
if isinstance(other, Money):
if self.currency != other.currency:
raise ValueError("货币单位不匹配")
return Money(self.amount + other.amount, self.currency)
elif isinstance(other, (int, float)):
return Money(self.amount + other, self.currency)
return NotImplemented
def __radd__(self, other):
return self.__add__(other)
def __iadd__(self, other):
result = self.__add__(other)
self.amount = result.amount
return self
def __sub__(self, other):
if isinstance(other, Money):
if self.currency != other.currency:
raise ValueError("货币单位不匹配")
return Money(self.amount - other.amount, self.currency)
elif isinstance(other, (int, float)):
return Money(self.amount - other, self.currency)
return NotImplemented
def __rsub__(self, other):
# other - self,注意顺序
if isinstance(other, (int, float)):
return Money(other - self.amount, self.currency)
return NotImplemented
def __mul__(self, factor: float):
if isinstance(factor, (int, float)):
return Money(self.amount * factor, self.currency)
return NotImplemented
def __rmul__(self, factor):
return self.__mul__(factor)
def __mod__(self, other):
"""取模:用于找零场景"""
if isinstance(other, Money):
if self.currency != other.currency:
raise ValueError("货币单位不匹配")
return Money(self.amount % other.amount, self.currency)
return NotImplemented
def __repr__(self):
return f"{self.currency} {self.amount:.2f}"
# 测试
m1 = Money(100.0)
m2 = Money(30.0)
print(m1 + m2) # CNY 130.00
print(50 + m1) # CNY 150.00(反向)
m1 += m2
print(m1) # CNY 130.00(就地)
print(m1 % Money(40)) # CNY 10.00(取模找零)
print(3 * m2) # CNY 90.00(反向乘法)
嗯,这一节内容不少。反向操作符和就地操作符是 Python 操作符重载里最容易忽略、也最容易出 bug 的地方。我的建议是:每次实现一个正向方法,就问问自己——「用户会不会把操作数反过来写?」、「用户会不会用 +=?」。如果答案是「会」,那就把对应的反向和就地方法也补上。