Python 3.12 Magic Methods - __mod__(self, other)
__mod__ 是 Python 中用于定义取模运算符 % 的核心魔术方法。它允许自定义类的实例支持取模运算,是实现整数模运算、循环索引、周期计算等功能的基础。取模运算通常返回除法后的余数,且与地板除 (//) 密切相关,满足恒等式 a = b * (a // b) + a % b。正确实现 __mod__ 可以让自定义类自然地融入 Python 的运算符体系,并与其他数值操作保持一致。本文将详细解析其定义、底层机制、设计原则,并通过多个示例逐行演示如何正确实现。
1. 定义与签名
python
def __mod__(self, other) -> object:
...
- 参数 :
self:当前对象(左操作数)。other:另一个操作数(右操作数),可以是任意类型。
- 返回值 :应返回一个新的对象,代表取模运算的结果。通常返回值类型与
self相同或为整数类型。如果运算未定义(例如类型不兼容),应返回单例NotImplemented。 - 调用时机 :
x % y会首先尝试调用x.__mod__(y)。- 如果
x.__mod__(y)返回NotImplemented,Python 会尝试调用y.__rmod__(x)(反向取模)。 - 如果两者都返回
NotImplemented,最终抛出TypeError。
2. 用途与典型场景
- 整数模运算 :自定义整数类型支持
%,如大整数、有限域元素。 - 循环索引 :例如使用
index % length实现循环缓冲区。 - 周期性计算 :角度归一化(如
angle % 360)、时间换算(如seconds % 60)。 - 数据结构实现:哈希表、循环队列中计算槽位。
- 与地板除配合 :实现
divmod函数或保持数学恒等式。
取模运算通常不满足交换律,因此 __rmod__ 需要独立实现。
3. 底层实现机制
在 Python/C API 层面,每个类型对象(PyTypeObject)都有一个 tp_as_number 结构体,其中包含 nb_remainder 槽位,这是一个函数指针,用于处理取模操作。当执行 x % y 时,解释器会:
- 获取
x的类型对象的tp_as_number结构。 - 如果存在
nb_remainder,则调用它,传入x和y,返回结果对象或Py_NotImplemented。 - 如果
x的nb_remainder返回Py_NotImplemented,则尝试获取y的类型对象的nb_remainder,并调用它,但此时参数顺序已交换(即调用y的__rmod__对应的 C 函数)。 - 如果仍然失败,则抛出
TypeError。
对于 Python 层定义的 __mod__,它会被包装到 nb_remainder 槽位中。而 __rmod__ 则对应同一个槽位的反向调用逻辑,但 Python 会在需要时自动调用右操作数的 __rmod__ 方法(如果存在)。
4. 设计原则与最佳实践
- 与地板除保持一致 :取模运算应与地板除(
__floordiv__)配合,满足恒等式a = b * (a // b) + a % b(当b不为零时)。这是 Python 中整数模运算的核心特性,自定义类也应遵循。 - 处理负数 :Python 的取模结果与右操作数符号一致,即
a % b的符号与b相同。例如-5 % 3的结果是1,而不是-2。自定义类应模仿这一行为,以保证与内置类型兼容。 - 返回新对象 :
__mod__通常不应修改操作数本身,而应返回一个新对象。 - 类型检查 :应检查
other的类型是否兼容,如果类型不匹配,应返回NotImplemented,而不是抛出异常。这样给另一操作数提供尝试反向运算的机会。 - 与
__rmod__的协作 :由于取模不满足交换律,__rmod__需要独立实现other % self的逻辑。 - 与
__ifloordiv__的区分 :__imod__用于就地取模(%=),通常应修改自身并返回self,适用于可变对象。 - 处理除零异常 :当
other为 0 时,应抛出ZeroDivisionError,这与内置行为一致。
5. 示例与逐行解析
示例 1:简单的整数包装类
python
class MyInt:
def __init__(self, value):
self.value = value
def __mod__(self, other):
# 处理 MyInt % 其他类型
if isinstance(other, MyInt):
if other.value == 0:
raise ZeroDivisionError("integer modulo by zero")
return MyInt(self.value % other.value)
if isinstance(other, int):
if other == 0:
raise ZeroDivisionError("integer modulo by zero")
return MyInt(self.value % other)
return NotImplemented
def __rmod__(self, other):
# 处理 其他类型 % MyInt
if isinstance(other, int):
if self.value == 0:
raise ZeroDivisionError("integer modulo by zero")
return MyInt(other % self.value)
return NotImplemented
def __floordiv__(self, other):
# 配合取模实现地板除
if isinstance(other, MyInt):
return MyInt(self.value // other.value)
if isinstance(other, int):
return MyInt(self.value // other)
return NotImplemented
def __repr__(self):
return f"MyInt({self.value})"
逐行解析:
| 行 | 代码 | 解释 |
|---|---|---|
| 1-3 | __init__ |
初始化整数值。 |
| 4-12 | __mod__ |
正向取模。 |
| 5-8 | 处理 MyInt 类型 |
检查除零,返回新 MyInt,值为 self.value % other.value。 |
| 9-11 | 处理普通整数 | 类似地处理整数。 |
| 12 | 返回 NotImplemented |
类型不支持时返回 NotImplemented。 |
| 13-19 | __rmod__ |
反向取模,处理 int % MyInt。独立实现,因为顺序相反。 |
| 20-26 | __floordiv__ |
实现地板除,与取模配合验证恒等式。 |
| 27-28 | __repr__ |
便于显示。 |
为什么这样写?
__rmod__独立实现,因为other % self与self % other不同,且 Python 的取模结果符号与右操作数相关。- 除零检查与内置行为一致。
- 返回新对象,保持不可变性。
- 同时实现
__floordiv__以支持恒等式验证。
验证:
python
a = MyInt(10)
b = MyInt(3)
print(a % b) # MyInt(1)
print(a % 4) # MyInt(2)
print(15 % a) # MyInt(5) → __rmod__
# 验证恒等式
c = MyInt(-10)
d = MyInt(3)
print(c // d) # MyInt(-4)
print(c % d) # MyInt(2)
print(-10, "=", 3, "*", (c // d).value, "+", (c % d).value) # -10 = 3 * -4 + 2
运行结果:
MyInt(1)
MyInt(2)
MyInt(5)
MyInt(-4)
MyInt(2)
-10 = 3 * -4 + 2
示例 2:处理负数的取模(遵循 Python 规则)
Python 的取模规则是:余数 r = a - b * floor(a/b),其中 floor 是向下取整。因此结果符号与除数 b 相同。
python
class MyInt:
def __init__(self, value):
self.value = value
def __mod__(self, other):
if isinstance(other, MyInt):
if other.value == 0:
raise ZeroDivisionError
# 使用内置 int 的取模,自动遵循 Python 规则
return MyInt(self.value % other.value)
if isinstance(other, int):
if other == 0:
raise ZeroDivisionError
return MyInt(self.value % other)
return NotImplemented
def __rmod__(self, other):
if isinstance(other, int):
if self.value == 0:
raise ZeroDivisionError
return MyInt(other % self.value)
return NotImplemented
def __repr__(self):
return f"MyInt({self.value})"
解析 :
这里直接复用内置 int 的取模运算,自动遵循 Python 的负数取模规则,无需手动处理。
验证:
python
a = MyInt(-10)
b = MyInt(3)
print(a % b) # MyInt(2) 因为 -10 % 3 = 2
print(a % -3) # MyInt(-1) 因为 -10 % -3 = -1
运行结果:
MyInt(2)
MyInt(-1)
示例 3:自定义有理数类(分数取模)
分数取模通常定义为 (a/b) % (c/d) = (a*d) % (b*c) / (b*d),但结果仍为分数?实际上,分数取模通常返回分数,但为简化,我们实现为返回 Fraction。
python
class Fraction:
def __init__(self, numerator, denominator=1):
if denominator == 0:
raise ZeroDivisionError
self.numerator = numerator
self.denominator = denominator
def __mod__(self, other):
# (a/b) % (c/d) = (a*d) % (b*c) / (b*d) ???实际上更复杂,但简化版本:
# 转换为同分母后取模
if isinstance(other, Fraction):
# 交叉相乘比较
num = self.numerator * other.denominator
den = self.denominator * other.numerator
# 取模结果的分子
mod_num = num % den
# 结果分母为 self.denominator * other.denominator
return Fraction(mod_num, self.denominator * other.denominator)
if isinstance(other, int):
return Fraction(self.numerator % (self.denominator * other), self.denominator)
return NotImplemented
def __repr__(self):
return f"Fraction({self.numerator}, {self.denominator})"
解析 :
这里简化了分数取模的实现,实际计算较复杂,但演示了 __mod__ 在自定义类中的应用。
验证:
python
f1 = Fraction(7, 3) # 7/3 ≈ 2.333
f2 = Fraction(2, 3) # 2/3 ≈ 0.666
print(f1 % f2) # 结果应为 (7/3) % (2/3) = (7%2)/3 = 1/3 → Fraction(1,3)
运行结果:
Fraction(3, 9)
示例 4:循环缓冲区索引
python
class CyclicBuffer:
def __init__(self, size):
self.size = size
self.index = 0
def advance(self, steps):
self.index = (self.index + steps) % self.size
def __mod__(self, other):
# 允许外部使用 % 计算循环索引
if isinstance(other, int):
return (self.index + other) % self.size
return NotImplemented
def __repr__(self):
return f"CyclicBuffer(size={self.size}, index={self.index})"
解析 :
__mod__ 在这里用于计算偏移后的索引,返回整数而非新对象,符合业务逻辑。
验证:
python
buf = CyclicBuffer(5)
print(buf % 2) # 2
buf.advance(4)
print(buf % 3) # (4+3)%5 = 2
运行结果:
2
2
示例 5:与 __rmod__ 配合(混合类型)
python
class MyInt:
def __init__(self, value):
self.value = value
def __mod__(self, other):
if isinstance(other, MyInt):
return MyInt(self.value % other.value)
if isinstance(other, int):
return MyInt(self.value % other)
return NotImplemented
def __rmod__(self, other):
if isinstance(other, int):
return MyInt(other % self.value)
return NotImplemented
def __repr__(self):
return f"MyInt({self.value})"
验证:
python
m = MyInt(4)
print(10 % m) # 10 % 4 = 2 → MyInt(2)
运行结果:
MyInt(2)
6. 与 __rmod__ 和 __imod__ 的关系
__rmod__(self, other):反向取模,用于other % self,当左操作数不支持时被调用。需独立实现。__imod__(self, other):用于%=就地取模,通常修改自身并返回self。如果未定义,则回退到self = self % other。
7. 注意事项与陷阱
- 不要修改
self:__mod__应返回新对象,除非类是可变的且你明确希望就地修改(但通常不这样做)。 - 正确使用
NotImplemented:当类型不兼容时返回NotImplemented,而不是None或False。这给另一侧机会处理。 - 避免无限递归 :在
__rmod__中不要调用self % other,那样会再次触发__mod__并可能导致循环(如果__mod__返回NotImplemented)。应直接实现逻辑。 - 处理除零异常 :当
other为 0 时,应抛出ZeroDivisionError,这与内置行为一致。 - 与地板除的一致性 :确保满足
a = b * (a // b) + a % b。如果不一致,可能导致意外行为。 - 负数处理 :遵循 Python 的取模规则,即结果符号与除数相同。可通过内置
%或手动计算实现。
8. 总结
| 特性 | 说明 |
|---|---|
| 角色 | 定义取模运算符 % |
| 签名 | __mod__(self, other) -> object |
| 返回值 | 新对象,或 NotImplemented |
| 调用时机 | x % y,以及反向尝试 |
| 底层 | C 层的 nb_remainder 槽位 |
与 __rmod__ 的关系 |
反向取模,用于 other % self,需独立实现 |
| 最佳实践 | 返回新对象、类型检查、使用 NotImplemented、处理除零、与 __floordiv__ 保持一致 |
掌握 __mod__ 是实现自定义数值类型的关键。通过理解其底层机制和设计原则,你可以构建出与 Python 内置类型一样自然、健壮的取模运算。无论是简单的整数包装,还是复杂的数学对象,正确实现 __mod__ 都能让代码更 Pythonic。
如果在学习过程中遇到问题,欢迎在评论区留言讨论!