欢迎光临
我们一直在努力

Python 3.12 MagicMethods - 42 - __divmod__

Python 3.12 Magic Method – __divmod__(self, other)


__divmod__ 是 Python 中用于实现内置函数 divmod() 的魔术方法。divmod(a, b) 返回一个元组 (a // b, a % b),即同时获取地板除和取模的结果。通过实现 __divmod__,自定义类可以支持 divmod(),并保证与 // 和 % 运算符的行为一致。本文将详细解析其定义、底层机制、设计原则,并通过多个示例逐行演示如何正确实现。


1. 定义与签名

def __divmod__(self, other) > tuple:
...

  • 参数:
    • self:当前对象(左操作数)。
    • other:另一个操作数(右操作数),可以是任意类型。
  • 返回值:必须返回一个二元元组 (quotient, remainder),其中 quotient 是 self // other 的结果,remainder 是 self % other 的结果。如果运算未定义(例如类型不兼容),应返回单例 NotImplemented。
  • 调用时机:
    • 调用 divmod(x, y) 时,首先尝试 x.__divmod__(y)。
    • 如果返回 NotImplemented,则尝试 y.__rdivmod__(x)(反向 __divmod__)。
    • 如果两者都返回 NotImplemented,最终抛出 TypeError。

2. 用途与典型场景

  • 同时获取商和余数:在自定义数值类型中,一次性计算两个值,避免重复调用 // 和 %。
  • 保证一致性:确保 // 和 % 运算与 divmod 返回的结果满足恒等式 a = b * q + r。
  • 性能优化:某些数据类型可能可以高效地同时计算商和余数,比分别调用两次更快(例如大整数算法)。
  • 数学对象:如多项式、矩阵、模运算等,可能定义自己的除法/取余操作。
  • 与内置函数 divmod 无缝集成:让自定义类像内置类型一样自然地使用 divmod。

3. 底层实现机制

在 Python/C API 层面,每个类型对象(PyTypeObject)都有一个 tp_as_number 结构体,其中包含 nb_divmod 槽位。这是一个函数指针,用于处理 divmod 操作。当执行 divmod(x, y) 时,解释器会:

  • 获取 x 的类型对象的 tp_as_number 结构。
  • 如果存在 nb_divmod,则调用它,传入 x 和 y,期望返回一个二元元组(或 Py_NotImplemented)。
  • 如果 x 的 nb_divmod 返回 Py_NotImplemented,则尝试获取 y 的类型对象的 nb_divmod,并调用它,但此时参数顺序已交换(即调用 y 的 __rdivmod__ 对应的 C 函数)。
  • 如果仍然失败,则抛出 TypeError。
  • 对于 Python 层定义的 __divmod__,它会被包装到 nb_divmod 槽位中。反向方法 __rdivmod__ 也会在必要时被调用。因此,实现 __divmod__ 时通常也应考虑实现 __rdivmod__ 以支持混合类型运算。


    4. 设计原则与最佳实践

    • 与 __floordiv__ 和 __mod__ 保持一致:__divmod__ 返回的元组必须等于 (self // other, self % other)。如果类实现了这两个运算符,应保证结果一致。
    • 返回元组:必须返回一个包含两个元素的元组。如果返回其他类型,Python 会尝试将其转换为元组,但可能导致错误。
    • 类型检查:应检查 other 的类型是否兼容,如果类型不匹配,应返回 NotImplemented,而不是抛出异常。这样给另一操作数提供尝试反向运算的机会。
    • 处理除零异常:当 other 为 0 时,应抛出 ZeroDivisionError,与内置 divmod 一致。
    • 返回新对象:商和余数通常应为新对象,不应修改原操作数(除非类是可变的)。
    • 实现反向方法:为了支持 divmod(other, self) 的场景,应实现 __rdivmod__。
    • 负数处理:遵循 Python 的取模规则(结果符号与除数一致),确保 // 和 % 满足恒等式。__divmod__ 应反映这一规则。

    5. 示例与逐行解析

    示例 1:简单的整数包装类(利用已有的 // 和 %)

    class MyInt:
    def __init__(self, value):
    self.value = value

    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 __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 __divmod__(self, other):
    # 利用已定义的 // 和 % 获取结果,打包成元组返回
    if isinstance(other, MyInt):
    return (self // other, self % other) # 注意:self // other 调用 __floordiv__
    if isinstance(other, int):
    return (self // other, self % other)
    return NotImplemented

    def __repr__(self):
    return f"MyInt({self.value})"

    逐行解析:

    行代码解释
    1-3 __init__ 初始化整数值。
    4-10 __floordiv__ 实现地板除,返回新 MyInt。
    11-17 __mod__ 实现取模,返回新 MyInt。
    18-24 __divmod__ 实现 divmod。
    19-21 处理 MyInt 类型 调用已定义的 __floordiv__ 和 __mod__ 获取结果,直接使用 self // other 和 self % other(触发相应方法),打包成元组返回。
    22-23 处理整数类型 同样调用相应方法,因为整数作为 other 时,// 和 % 也能工作(基于已实现的逻辑)。
    24 返回 NotImplemented 类型不支持时返回 NotImplemented,让 Python 尝试反向运算。
    25-26 __repr__ 便于显示。

    为什么这样写?

    • 复用已实现的 // 和 % 方法,避免重复逻辑,保证一致性。
    • 返回的元组包含两个 MyInt 对象,与 divmod 期望的返回值类型一致(内置 divmod 返回整数元组,这里是自定义类型元组)。
    • 类型检查后返回 NotImplemented,让 Python 有机会尝试反向运算(如 divmod(10, a))。

    验证:

    a = MyInt(10)
    b = MyInt(3)
    q, r = divmod(a, b)
    print(q) # MyInt(3)
    print(r) # MyInt(1)

    # 验证恒等式
    print(a.value == b.value * q.value + r.value) # True

    # 与整数混合
    q2, r2 = divmod(a, 4)
    print(q2) # MyInt(2)
    print(r2) # MyInt(2)

    运行结果:

    MyInt(3)
    MyInt(1)
    True
    MyInt(2)
    MyInt(2)

    示例 2:手动一次性计算(优化性能)

    对于某些类型,可能可以更高效地同时计算商和余数,避免两次调用方法。

    class MyInt:
    def __init__(self, value):
    self.value = value

    def __divmod__(self, other):
    if isinstance(other, MyInt):
    q = self.value // other.value
    r = self.value % other.value
    return (MyInt(q), MyInt(r))
    if isinstance(other, int):
    q = self.value // other
    r = self.value % other
    return (MyInt(q), MyInt(r))
    return NotImplemented

    解析:
    这里直接计算,没有调用 __floordiv__ 和 __mod__,可能更快。但注意要确保计算结果与单独调用 // 和 % 一致。

    验证:

    m1 = MyInt(10)
    m2 = MyInt(5)

    n1, n2 = divmod(m1, m2)
    print(n1, n2)
    print(divmod(m1, m2))

    运行结果:

    MyInt(2) MyInt(0)
    (MyInt(2), MyInt(0))

    示例 3:实现反向 __rdivmod__ 支持混合类型

    当左操作数为内置类型(如 int)而右操作数为自定义类型时,需要实现反向方法。

    class MyInt:
    def __init__(self, value):
    self.value = value

    def __divmod__(self, other):
    if isinstance(other, MyInt):
    return (self // other, self % other)
    return NotImplemented

    def __rdivmod__(self, other):
    # 处理 other (int) divmod self
    if isinstance(other, int):
    q = other // self.value
    r = other % self.value
    return (MyInt(q), MyInt(r))
    return NotImplemented

    def __floordiv__(self, other):
    if isinstance(other, MyInt):
    return MyInt(self.value // other.value)
    return NotImplemented

    def __mod__(self, other):
    if isinstance(other, MyInt):
    return MyInt(self.value % other.value)
    return NotImplemented

    def __repr__(self):
    return f"MyInt({self.value})"

    解析:

    • divmod(10, a) 先尝试 10.__divmod__(a),但 int 没有 __divmod__,返回 NotImplemented。
    • 然后尝试 a.__rdivmod__(10),成功返回结果。
    • __rdivmod__ 中手动计算商和余数,返回 MyInt 对象,保持类型一致。

    验证:

    a = MyInt(3)
    print(divmod(10, a)) # (MyInt(3), MyInt(1))

    运行结果:

    (MyInt(3), MyInt(1))

    示例 4:处理负数,保证与 // 和 % 一致

    Python 的 // 和 % 满足 a = b * (a // b) + a % b,且余数的符号与除数相同。实现 __divmod__ 时必须遵循这一规则。

    class MyInt:
    def __init__(self, value):
    self.value = value

    def __floordiv__(self, other):
    return MyInt(self.value // other.value)

    def __mod__(self, other):
    return MyInt(self.value % other.value)

    def __divmod__(self, other):
    # 直接利用 Python 的内置整数运算,自动处理负数
    q = self.value // other.value
    r = self.value % other.value
    return (MyInt(q), MyInt(r))

    解析:
    利用 Python 整数运算自动遵循取模规则,无需额外处理。

    验证:

    a = MyInt(10)
    b = MyInt(3)
    q, r = divmod(a, b)
    print(q) # MyInt(-4) 因为 -10 // 3 = -4
    print(r) # MyInt(2) 因为 -10 % 3 = 2

    运行结果:

    MyInt(-4)
    MyInt(2)


    6. 注意事项与陷阱

    • 不要修改 self:__divmod__ 应返回新对象,除非类是可变的且你明确希望就地修改(但通常不这样做)。
    • 正确使用 NotImplemented:当类型不兼容时返回 NotImplemented,而不是抛出异常。这给另一侧机会处理。
    • 与 __floordiv__ 和 __mod__ 的一致性:如果分别实现了这两个方法,__divmod__ 应返回与它们一致的结果。否则会导致不一致的行为,让用户困惑。
    • 处理除零异常:当 other 为 0 时,应抛出 ZeroDivisionError,这与内置 divmod 一致。
    • 返回类型:元组中的元素类型应与 __floordiv__ 和 __mod__ 返回的类型一致。例如,如果 // 返回 MyInt,% 返回 MyInt,那么 __divmod__ 也应返回 (MyInt, MyInt)。
    • 负数处理:确保遵循 Python 的取模规则,否则会导致数学错误。
    • 性能考虑:如果计算商和余数的方法可以一次完成(如大整数算法),应直接计算,避免两次调用。

    7. 总结

    特性说明
    角色 定义内置函数 divmod() 的行为
    签名 __divmod__(self, other) -> tuple
    返回值 二元元组 (quotient, remainder),或 NotImplemented
    调用时机 divmod(x, y),以及反向尝试
    底层 C 层的 nb_divmod 槽位
    与 __floordiv__/__mod__ 的关系 应保持一致,避免冗余计算
    最佳实践 返回新对象、类型检查、使用 NotImplemented、处理除零、实现反向方法

    掌握 __divmod__ 可以让自定义类支持 divmod 函数,并提供与 // 和 % 一致的语义。通过理解其底层机制和设计原则,你可以构建出更加完善和符合预期的数值类型。

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

    赞(0)
    未经允许不得转载:171主机测评 » Python 3.12 MagicMethods - 42 - __divmod__
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址