Python运算符重载:__add__、__eq__与容器方法实现
在 Python 中,运算符并不是孤立语法,3 + 5 实际会调用 (3).__add__(5),'hello' == 'world' 实际调用 'hello'.__eq__('world')。因此自定义类只要实现对应魔法方法,就能让 +、-、==、< 等运算符作用于对象。下面结合向量、金额、人员、版本号和矩阵几个例子,梳理算术运算、比较运算和容器类运算符重载的代码实现与注意事项。一、算术运算符重载:__add__、__sub__ 与金额类型
先看二维向量。实现 __add__ 和 __sub__ 后,v1 + v2、v1 - v2 会返回新的 Vector 对象:
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __sub__(self, other):
return Vector(self.x - other.x, self.y - other.y)
def __repr__(self):
return f'Vector({self.x}, {self.y})'
v1 = Vector(3, 4)
v2 = Vector(1, 2)
print(v1 + v2)# Vector(4, 6)
print(v1 - v2)# Vector(2, 2)
金额类 Money 更适合说明运算符重载中的边界处理。__add__ 和 __sub__ 先检查货币是否一致,不同货币直接抛出 ValueError。__mul__ 只接受 int 或 float,金额与数量相乘;遇到不支持的类型返回 NotImplemented,让 Python 继续尝试对方的反向运算或抛出 TypeError。__truediv__ 检查除数为 0,__floordiv__ 实现整除:
class Money:
def __init__(self, amount, currency='CNY'):
self.amount = amount
self.currency = currency
def __add__(self, other):
if self.currency != other.currency:
raise ValueError('不同货币不能直接相加')
return Money(self.amount + other.amount, self.currency)
def __sub__(self, other):
if self.currency != other.currency:
raise ValueError('不同货币不能直接相减')
return Money(self.amount - other.amount, self.currency)
def __mul__(self, factor):
if isinstance(factor, (int, float)):
return Money(self.amount * factor, self.currency)
return NotImplemented
def __truediv__(self, divisor):
if divisor == 0:
raise ZeroDivisionError('除数不能为0')
return Money(self.amount / divisor, self.currency)
def __floordiv__(self, divisor):
return Money(self.amount // divisor, self.currency)
def __repr__(self):
return f'Money({self.amount:.2f}, {self.currency})'
price = Money(100)
quantity = 3
total = price * quantity
print(total) # Money(300.00, CNY)
print(total / 2) # Money(150.00, CNY)
print(Money(500) - Money(200)) # Money(300.00, CNY)
这里的重点是:不支持的操作不要随意返回 None 或抛自定义异常,而是返回 NotImplemented。它表示当前类型没有实现这个运算,Python 会尝试调用右操作数的反向方法。
二、反向运算符与增强赋值:__radd__、__iadd__
当左操作数不支持某个运算时,Python 会尝试右操作数的反向方法。例如元组或列表 + Vector,会调用 Vector 的 __radd__。增强赋值 += 则会优先调用 __iadd__:
class Vector:
def __init__(self, x, y):
self.x, self.y = x, y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __radd__(self, other):
if isinstance(other, (tuple, list)) and len(other) == 2:
return Vector(self.x + other, self.y + other)
return NotImplemented
def __iadd__(self, other):
self.x += other.x
self.y += other.y
return self
def __repr__(self):
return f'Vector({self.x}, {self.y})'
v = Vector(3, 4)
# result = (1, 2) + v
# print(result)# Vector(4, 6)
v += Vector(1, 1)
print(v)# Vector(4, 5)
__iadd__ 修改对象自身后必须返回 self,否则 v += Vector(1, 1) 会把 v 绑定成 None 或其他非预期结果。__radd__ 的作用是处理左操作数不认识的运算,本例中允许二元 tuple 或 list 与 Vector 相加。
三、比较运算符:__eq__、__ne__ 与 __hash__ 的配合
Person 类用身份证号判断是否为同一个人。实现 __eq__ 后,再实现 __ne__ 反转结果。注意 __ne__ 遇到 NotImplemented 时也要直接返回 NotImplemented,不能简单地 not:
class Person:
def __init__(self, name, id_number):
self.name = name
self.id_number = id_number
def __eq__(self, other):
if not isinstance(other, Person):
return NotImplemented
return self.id_number == other.id_number
def __ne__(self, other):
result = self.__eq__(other)
if result is NotImplemented:
return result
return not result
def __hash__(self):
return hash(self.id_number)
def __repr__(self):
return f'Person({self.name}, {self.id_number})'
p1 = Person('张三', '110101199001011234')
p2 = Person('张三别名', '110101199001011234')
p3 = Person('李四', '110101199501011234')
print(p1 == p2)# True
print(p1 == p3)# False
print(p1 != p3)# True
people = {p1, p2, p3}
print(len(people))# 2
定义了 __eq__ 就必须同时定义 __hash__,否则对象放入 set 或作为 dict 的键时会出问题。集合去重同时依赖 __hash__ 和 __eq__,所以 p1 和 p2 虽然名字不同,但身份证号相同,会被视为同一个元素,最终集合长度为 2。
四、大小比较与 @total_ordering
版本号比较适合用 functools.total_ordering。只需要定义 __eq__ 和 __lt__,其余 <=、>、>= 由装饰器自动生成:
from functools import total_ordering
@total_ordering
class Version:
def __init__(self, major, minor, patch):
self.major = major
self.minor = minor
self.patch = patch
def __eq__(self, other):
return (
(self.major, self.minor, self.patch)
== (other.major, other.minor, other.patch)
)
def __lt__(self, other):
return (
(self.major, self.minor, self.patch)
< (other.major, other.minor, other.patch)
)
def __repr__(self):
return f'v{self.major}.{self.minor}.{self.patch}'
versions = [Version(2, 0, 1), Version(1, 5, 3),
Version(2, 1, 0), Version(1, 0, 0)]
print(sorted(versions))
#
这里把 major、minor、patch 组成元组后比较,sorted 能按语义化版本顺序输出。@total_ordering 适合比较规则清晰、只需要少写重复比较方法的场景。
五、容器类运算符:__getitem__、__setitem__、__len__ 与 __contains__
矩阵类 Matrix 通过 __getitem__ 支持 matrix,通过 __setitem__ 支持按行赋值,通过 __len__ 返回行数,通过 __contains__ 支持 in 判断:
class Matrix:
def __init__(self, rows, cols, default=0):
self.rows = rows
self.cols = cols
self._data = [ * cols for _ in range(rows)]
def __getitem__(self, index):
return self._data
def __setitem__(self, index, value):
if len(value) != self.cols:
raise ValueError(f'每行必须有{self.cols}个元素')
self._data = list(value)
def __len__(self):
return self.rows
def __contains__(self, value):
return any(value in row for row in self._data)
def __repr__(self):
return '\n'.join(' '.join(f'{v:3d}' for v in row) for row in self._data)
m = Matrix(3, 4)
m =
m =
m =
print(m) # 3
print(f'行数: {len(m)}')# 3
print(7 in m) # True
__getitem__ 返回行列表,所以 m 继续用列表索引取出元素。__setitem__ 在赋值时检查列数,列数不一致会抛出 ValueError。__len__ 返回 rows,因此 len(m) 得到行数。__contains__ 用 any 遍历每行,判断目标值是否出现在矩阵中。
六、常用运算符与魔法方法速查
除了上面例子,Python 运算符还对应一批魔法方法。整理如下,便于按场景查找:
# 算术运算符
# __add__(+), __sub__(-), __mul__(*), __truediv__(/), __floordiv__(//)
# __mod__(%), __pow__(**), __divmod__()
# 反向运算符
# __radd__, __rsub__, ...
# 增强赋值
# __iadd__, __isub__, ...
# 比较运算符
# __eq__(==), __ne__(!=), __lt__(<), __le__(<=), __gt__(>), __ge__(>=)
# 一元运算符
# __neg__(-), __pos__(+), __abs__(abs()), __invert__(~)
# 容器运算符
# __len__(len), __getitem__([]), __setitem__([]=), __delitem__(del [])
# __contains__(in), __iter__(for)
# 类型转换
# __int__(int), __float__(float), __bool__(bool), __str__(str), __bytes__(bytes)
# 其他
# __call__(obj()), __enter__/__exit__(with), __hash__(hash)
运算符重载的价值是让自定义对象用起来接近内置类型,但不应滥用。原则是行为符合直觉:+ 应表示加法或拼接,== 应表示相等比较。不支持的操作返回 NotImplemented,让 Python 尝试对方的反向方法;定义 __eq__ 时要考虑 __hash__;需要完整比较方法时可以用 @total_ordering 减少重复代码。
Re: Python运算符重载:__add__、__eq__与容器方法实现
楼主的整理很清晰,尤其是把 3 + 5 拆成 __add__ 调用,再把金额类的边界处理单独拎出来,这点很实用。NotImplemented 和自定义异常的区别确实容易踩坑:不支持的类型返回 NotImplemented,让 Python 去试反向运算;货币不一致这种语义错误才抛 ValueError,逻辑分得很明白。__iadd__ 里返回 self 也提醒得好,否则 a += b 后变量可能变成 None。反向运算用 __radd__ 接住元组或列表 + Vector 的写法挺巧。一个小建议是 Money.__add__ 里如果 other 不是 Money,先 isinstance 判断再返回 NotImplemented 会更稳,不然可能直接 AttributeError。标题里的 __eq__ 和容器方法正文好像还没展开,期待后续。Re: Python运算符重载:__add__、__eq__与容器方法实现
写得很清楚,从运算符实际调用魔法方法这个角度切入,很容易理解。Vector 和 Money 两个例子分别覆盖了返回新对象和业务边界,尤其 Money 里返回 NotImplemented 那段很关键,很多实现会直接返回 None 或抛异常,后面反向运算就接不上了。__radd__ 处理元组或列表、__iadd__ 原地修改并返回 self,这些细节也挺实用。小建议是,Vector 的 __add__ 和 __sub__ 如果 other 不是 Vector,直接取 other.x 会报 AttributeError,按 Money 的思路先做类型检查再返回 NotImplemented 会更统一;Money 的 __floordiv__ 也可以顺手补一下除数为 0 的检查。期待后面 __eq__ 和容器方法那部分,特别是比较运算与容器协议怎么配合。Re: Python运算符重载:__add__、__eq__与容器方法实现
写得很清楚,先把 3 + 5 背后调用 __add__ 这条线点出来,后面看 Vector 和 Money 就顺多了。Money 那里不同货币直接抛 ValueError、__mul__ 遇到不支持类型返回 NotImplemented,这个区分很实用,很多人会直接把异常吞掉或者返回 None,反而让报错变得难查。反向运算和 __iadd__ 的优先级也讲到了,实战里确实容易忽略。 不过帖子结尾好像断在 v = Vector(3, 4) 和 # result = ( 这里,__eq__ 和容器方法那部分还没看到。挺期待后面把 __eq__ 与 __hash__ 的配合、__len__、__getitem__、__contains__ 这些补上,尤其是切片、越界和比较时返回 NotImplemented 的处理。先等更新。
页:
[1]