Python 3.12 MagicMethods - 39 - __mod__

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 时,解释器会:

  1. 获取 x 的类型对象的 tp_as_number 结构。
  2. 如果存在 nb_remainder,则调用它,传入 xy,返回结果对象或 Py_NotImplemented
  3. 如果 xnb_remainder 返回 Py_NotImplemented,则尝试获取 y 的类型对象的 nb_remainder,并调用它,但此时参数顺序已交换(即调用 y__rmod__ 对应的 C 函数)。
  4. 如果仍然失败,则抛出 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 % selfself % 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,而不是 NoneFalse。这给另一侧机会处理。
  • 避免无限递归 :在 __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。


如果在学习过程中遇到问题,欢迎在评论区留言讨论!

相关推荐
小二·2 小时前
Go 语言系统编程与云原生开发实战(第40篇 · 终章)
开发语言·云原生·golang
小鸡吃米…2 小时前
Python 中的并发 —— 简介
服务器·数据库·python
格林威2 小时前
工业相机图像高速存储(C++版):内存映射文件(MMF)零拷贝方案,附海康相机实战代码!
开发语言·c++·数码相机·计算机视觉·视觉检测·工业相机·海康相机
无限进步_2 小时前
深入解析string:从设计思想到完整实现
开发语言·c++·ide·windows·git·github·visual studio
melonbo2 小时前
C++ 中用于模块间通信的设计模式
开发语言·c++·设计模式
咋吃都不胖lyh2 小时前
WSL2(Linux)+ VSCode 运行 D 盘 Python 文件全流程文档
linux·vscode·python
进击的雷神2 小时前
请求频率限制、嵌套数据结构、多目录聚合、地址字段重构——K展爬虫四大技术难关攻克纪实
数据结构·爬虫·python·重构
老师好,我是刘同学2 小时前
Python字符串全解析:从创建到实战应用
python
王的宝库2 小时前
Go 语言基础进阶:指针、init、匿名函数/闭包、defer
开发语言·go