Python 3.12 MagicMethods - 28 - __rsub__

Python 3.12 Magic Method - __rsub__(self, other)


__rsub__ 是 Python 中用于定义反向减法 的魔术方法。当左操作数不支持与右操作数的减法时,Python 会尝试调用右操作数的 __rsub__ 方法,从而实现 other - self 的运算。由于减法不满足交换律,__rsub__ 的实现通常与 __sub__ 不同,需要独立处理。本文将详细解析其定义、底层机制、设计原则,并通过多个示例逐行演示如何正确实现。


1. 定义与签名

python 复制代码
def __rsub__(self, other) -> object:
    ...
  • 参数
    • self:当前对象(右操作数,因为它是被调用的对象)。
    • other:另一个操作数(左操作数),可以是任意类型。
  • 返回值 :应返回一个新的对象,代表 other - self 的结果。如果运算未定义,应返回单例 NotImplemented
  • 调用时机 :当左操作数 x 不支持与右操作数 y 的减法时(即 x.__sub__(y) 返回 NotImplemented),Python 会尝试调用 y.__rsub__(x)

2. 为什么需要 __rsub__

考虑一个自定义类 Vector,我们希望它支持与标量相减,如 vec - 5。这可以通过实现 __sub__ 并检查标量类型来实现。但如果我们希望 5 - vec 也能工作呢?int 类型的 __sub__ 并不知道如何处理 Vector,它会返回 NotImplemented。此时,Python 会调用 vec.__rsub__(5),从而让我们的类有机会处理这个运算。这就是 __rsub__ 的作用------支持左操作数为其他类型的减法

与加法不同,减法不满足交换律 ,因此 5 - vec 的结果通常不等于 vec - 5。所以 __rsub__ 不能简单地委托给 __sub__,而需要独立实现。


3. 调用时机与优先级

当执行 x - y 时,Python 的运算符分发机制如下:

  1. 尝试调用 x.__sub__(y)
  2. 如果 x.__sub__(y) 返回 NotImplemented,则尝试调用 y.__rsub__(x)
  3. 如果 y.__rsub__(x) 也返回 NotImplemented,则抛出 TypeError

注意:__rsub__ 只有在左操作数无法处理时才会被调用,且不会先于左操作数的 __sub__


4. 底层实现机制

在 CPython 中,减法操作由 PyNumber_Subtract 函数处理,其逻辑与加法类似:

c 复制代码
PyObject *PyNumber_Subtract(PyObject *v, PyObject *w) {
    PyObject *result = binary_op1(v, w, NB_SLOT(nb_subtract));
    if (result == Py_NotImplemented) {
        Py_DECREF(result);
        result = binary_op1(w, v, NB_SLOT(nb_subtract));
    }
    return result;
}

这里的 binary_op1 会调用对象的 tp_as_number.nb_subtract 槽位。对于 Python 定义的类,__sub____rsub__ 都被包装到 nb_subtract 槽位中,但通过参数顺序区分:当 binary_op1 被第一次调用时(v 是左操作数),它调用的是 v__sub__;当第二次调用时(参数交换),它调用的是 w__rsub__。因此,我们不需要为 __rsub__ 设置独立的 C 槽位,Python 会在需要时自动调用右操作数的 __rsub__ 方法。


5. 设计原则与最佳实践

  • 独立实现 :由于减法不满足交换律,__rsub__ 必须实现 other - self 的语义,不能简单地委托给 __sub__
  • 类型检查 :应检查 other 的类型是否兼容,如果类型不匹配,返回 NotImplemented
  • 返回新对象 :通常应返回新对象,除非类是可变的且你希望就地修改(但 __rsub__ 很少用于可变对象,因为 other 可能不是同类)。
  • 处理常见情况 :通常处理 other 为数字、字符串或其他内置类型的情况,使类更易用。
  • 避免无限递归 :不要在 __rsub__ 中调用 self - otherother - self 的另一种形式,以免导致循环。
  • __sub__ 的对称性 :确保 a - bb - a 的结果符合数学直觉(即使不同)。

6. 示例与逐行解析

示例 1:支持标量反向减法的向量类

python 复制代码
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __sub__(self, other):
        # 处理 Vector - Vector
        if isinstance(other, Vector):
            return Vector(self.x - other.x, self.y - other.y)
        # 处理 Vector - 标量
        if isinstance(other, (int, float)):
            return Vector(self.x - other, self.y - other)
        return NotImplemented

    def __rsub__(self, other):
        # 处理 标量 - Vector
        if isinstance(other, (int, float)):
            return Vector(other - self.x, other - self.y)
        return NotImplemented

    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

逐行解析

代码 解释
1-4 __init__ 初始化坐标。
5-11 __sub__ 正向减法,处理同类型和标量。
12-16 __rsub__ 反向减法,处理标量减向量。注意这里 other 是标量,我们返回新向量 (other - x, other - y),与正向减法相反。
17-18 __repr__ 便于显示。

为什么这样写?

  • __rsub__ 不能调用 self.__sub__(other),因为那会计算 self - other,而我们想要的是 other - self
  • 类型检查确保只有数字类型被处理,其他类型返回 NotImplemented,让 Python 最终抛出 TypeError

验证:

python 复制代码
v = Vector(1, 2)
print(v - 5)      # Vector(-4, -3)  → __sub__
print(5 - v)      # Vector(4, 3)    → __rsub__

运行结果:

复制代码
Vector(-4, -3)
Vector(4, 3)

示例 2:处理不同类型之间的反向减法

python 复制代码
class Money:
    def __init__(self, amount, currency="CNY"):
        self.amount = amount
        self.currency = currency

    def __sub__(self, other):
        if isinstance(other, Money):
            if self.currency != other.currency:
                raise ValueError("Currency mismatch")
            return Money(self.amount - other.amount, self.currency)
        if isinstance(other, (int, float)):
            return Money(self.amount - other, self.currency)
        return NotImplemented

    def __rsub__(self, other):
        # 处理 数字 - Money
        if isinstance(other, (int, float)):
            # 假设数字与当前货币兼容
            return Money(other - self.amount, self.currency)
        return NotImplemented

    def __repr__(self):
        return f"Money({self.amount}, {self.currency})"

验证:

python 复制代码
m = Money(100)
print(50 - m)   #  → __rsub__

运行结果 :

复制代码
Money(-50, CNY)

解析

  • 50 - m 触发 __rsub__,返回金额为 50 - 100 = -50 的新 Money 对象,货币不变。
  • 注意这里没有检查货币,因为左操作数是数字,我们假设数字代表相同货币。

示例 3:反向减法返回不同类型

有时反向减法可能需要返回不同类型,例如 datetime 模块中 date - date 返回 timedelta,而 date - timedelta 返回 date。我们可以模拟类似行为:

python 复制代码
class MyDate:
    def __init__(self, days):
        self.days = days

    def __sub__(self, other):
        if isinstance(other, MyDate):
            return MyDelta(self.days - other.days)   # 返回差值对象
        if isinstance(other, MyDelta):
            return MyDate(self.days - other.delta)   # 返回新日期
        return NotImplemented

    def __rsub__(self, other):
        if isinstance(other, MyDelta):
            return MyDate(other.delta - self.days)   # other.delta - self.days
        return NotImplemented

    def __repr__(self):
        return f"MyDate({self.days})"

class MyDelta:
    def __init__(self, delta):
        self.delta = delta

    def __repr__(self):
        return f"MyDelta({self.delta})"

验证

python 复制代码
d = MyDate(10)
delta = MyDelta(3)
print(d - delta)   # MyDate(7)       → __sub__
print(delta - d)   # MyDate(-7)      → __rsub__ 

运行结果:

复制代码
MyDate(7)
MyDate(-7)

解析 :这里 __rsub__ 处理了 MyDelta - MyDate 的情况,返回一个新的 MyDate,体现了反向减法返回不同类型的能力。

示例 4:不实现 __rsub__ 的情况

如果类没有实现 __rsub__,尝试反向减法会导致 TypeError

python 复制代码
class Simple:
    def __sub__(self, other):
        return "Simple - other"

验证:

python 复制代码
s = Simple()
print(5 - s)   # TypeError: unsupported operand type(s) for -: 'int' and 'Simple'

运行结果:

复制代码
Traceback (most recent call last):
  File "\py_magicmethods_rsub_04.py", line xx, in <module>
    print(5 - s)   # TypeError: unsupported operand type(s) for -: 'int' and 'Simple'
          ~~^~~
TypeError: unsupported operand type(s) for -: 'int' and 'Simple'

解析:

因为没有 __rsub__int__sub__ 返回 NotImplemented,然后尝试 s.__rsub__(5) 失败(不存在),最终抛出 TypeError


7. 与 __sub____isub__ 的关系

  • __sub__(self, other) :定义 self - other,返回新对象。
  • __rsub__(self, other) :定义 other - self,返回新对象,不满足交换律
  • __isub__(self, other) :定义 -= 就地减法,修改自身并返回 self。如果未定义,回退到 self = self - other(调用 __sub__)。

8. 注意事项与陷阱

  • 不要调用 self - other :在 __rsub__ 中调用 self - other 会再次触发正向减法,可能导致无限递归(如果正向减法返回 NotImplemented)。应直接实现运算逻辑。
  • 正确处理类型不匹配 :返回 NotImplemented 而非抛出异常,以给其他类型机会。
  • 考虑与 __sub__ 的一致性 :确保 a - bb - a 的语义合理,即使结果不同。
  • 对于可变对象__rsub__ 通常返回新对象,因为右操作数可能是不可变类型。

9. 总结

特性 说明
角色 定义反向减法 other - self
签名 __rsub__(self, other) -> object
调用时机 左操作数的 __sub__ 返回 NotImplemented
返回值 新对象,或 NotImplemented
底层 由 Python 的运算符分发机制在 C 层处理,通过交换参数调用右操作数的 __rsub__
__sub__ 的关系 独立实现,不满足交换律
最佳实践 类型检查、返回 NotImplemented、直接实现运算逻辑、避免调用 self - other

掌握 __rsub__ 是实现自定义类与 Python 运算符生态无缝集成的关键,尤其是在处理混合类型运算时。通过理解其底层机制和设计原则,你可以让类支持更自然的减法语义,提升代码的可读性和可用性。

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

相关推荐
李可以量化2 小时前
用 KMeans 聚类寻找股票支撑位与压力位(上):基于 QMT 量化平台实现
python·量化 qmt ptrade
所谓伊人,在水一方3332 小时前
【Python数据科学实战之路】第12章 | 无监督学习算法实战:聚类与降维的奥秘
python·sql·学习·算法·信息可视化·聚类
MoRanzhi12032 小时前
Pillow 灰度化、二值化与阈值处理
图像处理·python·pillow·二值化·图像预处理·阈值处理·灰度化
饕餮争锋2 小时前
Java泛型介绍
java·开发语言
飞Link3 小时前
告别复杂调参:Prophet 加法模型深度解析与实战
开发语言·python·数据挖掘
测试人社区—66793 小时前
当代码面临道德选择:VR如何为AI伦理决策注入“人性压力”
网络·人工智能·python·microsoft·vr·azure
独行soc3 小时前
2026年渗透测试面试题总结-36(题目+回答)
网络·python·安全·web安全·网络安全·渗透测试·安全狮
zh_xuan3 小时前
测试go语言函数和结构体
开发语言·golang
witAI3 小时前
**Kimi小说灵感2025推荐,从零到一的创意激发指南**
人工智能·python