在 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[0], self.y + other[1])
- 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))
- # [v1.0.0, v1.5.3, v2.0.1, v2.1.0]
复制代码
这里把 major、minor、patch 组成元组后比较,sorted 能按语义化版本顺序输出。@total_ordering 适合比较规则清晰、只需要少写重复比较方法的场景。
五、容器类运算符:__getitem__、__setitem__、__len__ 与 __contains__
矩阵类 Matrix 通过 __getitem__ 支持 matrix[row][col],通过 __setitem__ 支持按行赋值,通过 __len__ 返回行数,通过 __contains__ 支持 in 判断:
- class Matrix:
- def __init__(self, rows, cols, default=0):
- self.rows = rows
- self.cols = cols
- self._data = [[default] * cols for _ in range(rows)]
- def __getitem__(self, index):
- return self._data[index]
- def __setitem__(self, index, value):
- if len(value) != self.cols:
- raise ValueError(f'每行必须有{self.cols}个元素')
- self._data[index] = 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[0] = [1, 2, 3, 4]
- m[1] = [5, 6, 7, 8]
- m[2] = [9, 10, 11, 12]
- print(m[0][2]) # 3
- print(f'行数: {len(m)}') # 3
- print(7 in m) # True
复制代码
__getitem__ 返回行列表,所以 m[0][2] 继续用列表索引取出元素。__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 减少重复代码。 |