查看: 285|回复: 0

Python运算符重载:__add__、__eq__与容器方法实现

[复制链接]
发表于 2 小时前 | 显示全部楼层 |阅读模式
在 Python 中,运算符并不是孤立语法,3 + 5 实际会调用 (3).__add__(5),'hello' == 'world' 实际调用 'hello'.__eq__('world')。因此自定义类只要实现对应魔法方法,就能让 +、-、==、< 等运算符作用于对象。下面结合向量、金额、人员、版本号和矩阵几个例子,梳理算术运算、比较运算和容器类运算符重载的代码实现与注意事项。

一、算术运算符重载:__add__、__sub__ 与金额类型

先看二维向量。实现 __add__ 和 __sub__ 后,v1 + v2、v1 - v2 会返回新的 Vector 对象:
  1. class Vector:
  2.     def __init__(self, x, y):
  3.         self.x = x
  4.         self.y = y
  5.     def __add__(self, other):
  6.         return Vector(self.x + other.x, self.y + other.y)
  7.     def __sub__(self, other):
  8.         return Vector(self.x - other.x, self.y - other.y)
  9.     def __repr__(self):
  10.         return f'Vector({self.x}, {self.y})'
  11. v1 = Vector(3, 4)
  12. v2 = Vector(1, 2)
  13. print(v1 + v2)  # Vector(4, 6)
  14. print(v1 - v2)  # Vector(2, 2)
复制代码

金额类 Money 更适合说明运算符重载中的边界处理。__add__ 和 __sub__ 先检查货币是否一致,不同货币直接抛出 ValueError。__mul__ 只接受 int 或 float,金额与数量相乘;遇到不支持的类型返回 NotImplemented,让 Python 继续尝试对方的反向运算或抛出 TypeError。__truediv__ 检查除数为 0,__floordiv__ 实现整除:
  1. class Money:
  2.     def __init__(self, amount, currency='CNY'):
  3.         self.amount = amount
  4.         self.currency = currency
  5.     def __add__(self, other):
  6.         if self.currency != other.currency:
  7.             raise ValueError('不同货币不能直接相加')
  8.         return Money(self.amount + other.amount, self.currency)
  9.     def __sub__(self, other):
  10.         if self.currency != other.currency:
  11.             raise ValueError('不同货币不能直接相减')
  12.         return Money(self.amount - other.amount, self.currency)
  13.     def __mul__(self, factor):
  14.         if isinstance(factor, (int, float)):
  15.             return Money(self.amount * factor, self.currency)
  16.         return NotImplemented
  17.     def __truediv__(self, divisor):
  18.         if divisor == 0:
  19.             raise ZeroDivisionError('除数不能为0')
  20.         return Money(self.amount / divisor, self.currency)
  21.     def __floordiv__(self, divisor):
  22.         return Money(self.amount // divisor, self.currency)
  23.     def __repr__(self):
  24.         return f'Money({self.amount:.2f}, {self.currency})'
  25. price = Money(100)
  26. quantity = 3
  27. total = price * quantity
  28. print(total)                   # Money(300.00, CNY)
  29. print(total / 2)               # Money(150.00, CNY)
  30. print(Money(500) - Money(200)) # Money(300.00, CNY)
复制代码

这里的重点是:不支持的操作不要随意返回 None 或抛自定义异常,而是返回 NotImplemented。它表示当前类型没有实现这个运算,Python 会尝试调用右操作数的反向方法。

二、反向运算符与增强赋值:__radd__、__iadd__

当左操作数不支持某个运算时,Python 会尝试右操作数的反向方法。例如元组或列表 + Vector,会调用 Vector 的 __radd__。增强赋值 += 则会优先调用 __iadd__:
  1. class Vector:
  2.     def __init__(self, x, y):
  3.         self.x, self.y = x, y
  4.     def __add__(self, other):
  5.         return Vector(self.x + other.x, self.y + other.y)
  6.     def __radd__(self, other):
  7.         if isinstance(other, (tuple, list)) and len(other) == 2:
  8.             return Vector(self.x + other[0], self.y + other[1])
  9.         return NotImplemented
  10.     def __iadd__(self, other):
  11.         self.x += other.x
  12.         self.y += other.y
  13.         return self
  14.     def __repr__(self):
  15.         return f'Vector({self.x}, {self.y})'
  16. v = Vector(3, 4)
  17. # result = (1, 2) + v
  18. # print(result)  # Vector(4, 6)
  19. v += Vector(1, 1)
  20. 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:
  1. class Person:
  2.     def __init__(self, name, id_number):
  3.         self.name = name
  4.         self.id_number = id_number
  5.     def __eq__(self, other):
  6.         if not isinstance(other, Person):
  7.             return NotImplemented
  8.         return self.id_number == other.id_number
  9.     def __ne__(self, other):
  10.         result = self.__eq__(other)
  11.         if result is NotImplemented:
  12.             return result
  13.         return not result
  14.     def __hash__(self):
  15.         return hash(self.id_number)
  16.     def __repr__(self):
  17.         return f'Person({self.name}, {self.id_number})'
  18. p1 = Person('张三', '110101199001011234')
  19. p2 = Person('张三别名', '110101199001011234')
  20. p3 = Person('李四', '110101199501011234')
  21. print(p1 == p2)  # True
  22. print(p1 == p3)  # False
  23. print(p1 != p3)  # True
  24. people = {p1, p2, p3}
  25. print(len(people))  # 2
复制代码

定义了 __eq__ 就必须同时定义 __hash__,否则对象放入 set 或作为 dict 的键时会出问题。集合去重同时依赖 __hash__ 和 __eq__,所以 p1 和 p2 虽然名字不同,但身份证号相同,会被视为同一个元素,最终集合长度为 2。

四、大小比较与 @total_ordering

版本号比较适合用 functools.total_ordering。只需要定义 __eq__ 和 __lt__,其余 <=、>、>= 由装饰器自动生成:
  1. from functools import total_ordering
  2. @total_ordering
  3. class Version:
  4.     def __init__(self, major, minor, patch):
  5.         self.major = major
  6.         self.minor = minor
  7.         self.patch = patch
  8.     def __eq__(self, other):
  9.         return (
  10.             (self.major, self.minor, self.patch)
  11.             == (other.major, other.minor, other.patch)
  12.         )
  13.     def __lt__(self, other):
  14.         return (
  15.             (self.major, self.minor, self.patch)
  16.             < (other.major, other.minor, other.patch)
  17.         )
  18.     def __repr__(self):
  19.         return f'v{self.major}.{self.minor}.{self.patch}'
  20. versions = [Version(2, 0, 1), Version(1, 5, 3),
  21.             Version(2, 1, 0), Version(1, 0, 0)]
  22. print(sorted(versions))
  23. # [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 判断:
  1. class Matrix:
  2.     def __init__(self, rows, cols, default=0):
  3.         self.rows = rows
  4.         self.cols = cols
  5.         self._data = [[default] * cols for _ in range(rows)]
  6.     def __getitem__(self, index):
  7.         return self._data[index]
  8.     def __setitem__(self, index, value):
  9.         if len(value) != self.cols:
  10.             raise ValueError(f'每行必须有{self.cols}个元素')
  11.         self._data[index] = list(value)
  12.     def __len__(self):
  13.         return self.rows
  14.     def __contains__(self, value):
  15.         return any(value in row for row in self._data)
  16.     def __repr__(self):
  17.         return '\n'.join(' '.join(f'{v:3d}' for v in row) for row in self._data)
  18. m = Matrix(3, 4)
  19. m[0] = [1, 2, 3, 4]
  20. m[1] = [5, 6, 7, 8]
  21. m[2] = [9, 10, 11, 12]
  22. print(m[0][2])          # 3
  23. print(f'行数: {len(m)}')  # 3
  24. print(7 in m)           # True
复制代码

__getitem__ 返回行列表,所以 m[0][2] 继续用列表索引取出元素。__setitem__ 在赋值时检查列数,列数不一致会抛出 ValueError。__len__ 返回 rows,因此 len(m) 得到行数。__contains__ 用 any 遍历每行,判断目标值是否出现在矩阵中。

六、常用运算符与魔法方法速查

除了上面例子,Python 运算符还对应一批魔法方法。整理如下,便于按场景查找:
  1. # 算术运算符
  2. # __add__(+), __sub__(-), __mul__(*), __truediv__(/), __floordiv__(//)
  3. # __mod__(%), __pow__(**), __divmod__()
  4. # 反向运算符
  5. # __radd__, __rsub__, ...
  6. # 增强赋值
  7. # __iadd__, __isub__, ...
  8. # 比较运算符
  9. # __eq__(==), __ne__(!=), __lt__(<), __le__(<=), __gt__(>), __ge__(>=)
  10. # 一元运算符
  11. # __neg__(-), __pos__(+), __abs__(abs()), __invert__(~)
  12. # 容器运算符
  13. # __len__(len), __getitem__([]), __setitem__([]=), __delitem__(del [])
  14. # __contains__(in), __iter__(for)
  15. # 类型转换
  16. # __int__(int), __float__(float), __bool__(bool), __str__(str), __bytes__(bytes)
  17. # 其他
  18. # __call__(obj()), __enter__/__exit__(with), __hash__(hash)
复制代码

运算符重载的价值是让自定义对象用起来接近内置类型,但不应滥用。原则是行为符合直觉:+ 应表示加法或拼接,== 应表示相等比较。不支持的操作返回 NotImplemented,让 Python 尝试对方的反向方法;定义 __eq__ 时要考虑 __hash__;需要完整比较方法时可以用 @total_ordering 减少重复代码。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-9-15 14:58 , Processed in 0.022791 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部