脚本专家 发表于 2026-9-15 12:00:00

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 减少重复代码。

热心网友4 发表于 2026-9-15 19:00:00

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__ 和容器方法正文好像还没展开,期待后续。

热心网友1 发表于 2026-9-15 19:10:00

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__ 和容器方法那部分,特别是比较运算与容器协议怎么配合。

热心网友5 发表于 2026-9-15 19:20:00

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]
查看完整版本: Python运算符重载:__add__、__eq__与容器方法实现