Python 3.12 MagicMethods - 48 - __rmatmul__

Python 3.12 Magic Method - __rmatmul__(self, other)


__rmatmul__ 是 Python 中用于定义 反向矩阵乘法 的魔术方法。当左操作数不支持与右操作数的 @ 运算时,Python 会尝试调用右操作数的 __rmatmul__ 方法,从而实现 other @ self 的运算。由于矩阵乘法通常不满足交换律,__rmatmul__ 的实现必须独立处理,不能简单地委托给 __matmul__ 。本文将详细解析其定义、底层机制、设计原则,并通过多个示例逐行演示如何正确实现。


1. 定义与签名

python 复制代码
def __rmatmul__(self, other) -> object:
    ...
  • 参数
    • self:当前对象(右操作数,因为它是被调用的对象)。
    • other:另一个操作数(左操作数),可以是任意类型。
  • 返回值 :应返回一个新的对象,代表 other @ self 的结果。如果运算未定义,应返回单例 NotImplemented
  • 调用时机 :当执行 x @ y 时 :
    • 首先尝试调用 x.__matmul__(y)
    • 如果 x.__matmul__(y) 返回 NotImplemented,则尝试调用 y.__rmatmul__(x)
    • 如果两者都返回 NotImplemented,最终抛出 TypeError

2. 为什么需要 __rmatmul__

矩阵乘法是线性代数中的核心运算,不满足交换律A @ B 通常不等于 B @ A。因此,Python 的运算符分发机制必须能够正确处理两种顺序的运算。

考虑一个自定义矩阵类 Matrix,我们希望它支持与标量(或其他类型)的乘法:

  • Matrix(2,3) @ scalar(正向)可以通过 __matmul__ 实现。
  • scalar @ Matrix(2,3)(反向)则需要 __rmatmul__,因为内置类型(如 int)的 __matmul__ 不知道如何处理 Matrix,会返回 NotImplemented

如果左操作数不支持运算,Python 会调用右操作数的反向方法,让你的类有机会定义 other @ self 的语义。这正是 __rmatmul__ 的作用------支持左操作数为其他类型的矩阵乘法


3. 底层实现机制

在 Python 的 C 层,矩阵乘法的处理遵循标准的反向运算符查找机制。@ 运算符对应 tp_as_number.nb_matrix_multiply 槽位 。当执行 x @ y 时,解释器会:

  1. 调用 x.__matmul__(y)(C 层对应 nb_matrix_multiply)。
  2. 如果返回 NotImplemented,则调用 y.__rmatmul__(x)(C 层通过交换参数再次调用 nb_matrix_multiply,但 Python 会自动将这次调用定向到 __rmatmul__ )。
  3. 如果仍返回 NotImplemented,抛出 TypeError

这种机制确保了非交换运算的正确性。如果直接使用 y.__matmul__(x) 代替反向调用,结果将是错误的,因为 x @ yy @ x 语义不同 。因此,__rmatmul__ 是专门为右操作数设计的独立方法。


4. 设计原则与最佳实践

  • 独立实现 :由于矩阵乘法不满足交换律,__rmatmul__ 必须实现 other @ self 的语义,不能简单地委托给 __matmul__(除非你明确知道二者等价,比如标量与矩阵的乘法通常可交换,但即使如此也应独立实现以保证清晰性)。
  • 类型检查 :应检查 other 的类型是否兼容。如果类型不匹配,返回 NotImplemented 。不要抛出异常,这样给其他类型机会处理。
  • 返回新对象 :矩阵乘法通常应返回新对象,不修改原操作数(除非类是可变的且你使用 @= 运算符)。
  • 遵循数学规则 :对于矩阵乘法,必须检查维度兼容性(如果适用),否则应抛出 ValueError 。对于标量与矩阵的乘法,通常定义为数乘(每个元素乘以标量),这可以看作是一种交换运算。
  • 避免无限递归 :在 __rmatmul__ 中不要调用 self @ otherother @ self 的另一种形式,否则可能导致循环。

5. 示例与逐行解析

示例 1:基本矩阵类实现反向数乘

python 复制代码
class Matrix:
    def __init__(self, data):
        self.data = data
        self.rows = len(data)
        self.cols = len(data[0]) if data else 0

    def __matmul__(self, other):
        # 正向矩阵乘法:self @ other
        if isinstance(other, Matrix):
            # 正常矩阵乘法(省略具体实现,假设已定义)
            pass
        return NotImplemented

    def __rmatmul__(self, other):
        # 反向矩阵乘法:other @ self
        # 这里定义标量左乘矩阵为数乘(每个元素乘以标量)
        if isinstance(other, (int, float)):
            result = [[other * val for val in row] for row in self.data]
            return Matrix(result)
        # 如果 other 也是 Matrix,但需要反向矩阵乘法(不常见,这里也处理)
        if isinstance(other, Matrix):
            # 注意:这里必须实现 other @ self,即 other 乘 self
            if other.cols != self.rows:
                raise ValueError("Incompatible dimensions for matrix multiplication")
            # 计算 other @ self
            result = [[0 for _ in range(self.cols)] for _ in range(other.rows)]
            for i in range(other.rows):
                for j in range(self.cols):
                    for k in range(other.cols):
                        result[i][j] += other.data[i][k] * self.data[k][j]
            return Matrix(result)
        return NotImplemented

    def __repr__(self):
        return '\n'.join([' '.join(map(str, row)) for row in self.data])

逐行解析

代码 解释
1-5 __init__ 初始化矩阵。
6-10 __matmul__ 正向乘法,这里简化为只处理同类型,返回 NotImplemented 以便测试反向。
11-26 __rmatmul__ 反向矩阵乘法。
12-16 处理标量 如果 other 是数字,返回新矩阵,每个元素乘以该数字(数乘)。这是常见的交换运算。
17-24 处理矩阵 如果 other 也是 Matrix,实现 other @ self。需要检查维度,然后计算矩阵乘法。
25 返回 NotImplemented 其他类型不处理。
27-28 __repr__ 便于显示。

为什么这样写?

  • __rmatmul__ 独立处理两种可能:标量和矩阵。对于标量,数乘通常是对称的,所以这里的结果与 self @ other 一致(如果 self 也有数乘的话)。但我们仍然独立实现,避免依赖正向方法。
  • 对于矩阵,必须计算 other @ self,不能使用 self @ other,因为顺序相反。

验证:

python 复制代码
M = Matrix([[1, 2], [3, 4]])
result = 2 @ M   # 调用 M.__rmatmul__(2)
print(result)

运行结果:

复制代码
2 4
6 8

示例 2:标量与矩阵的乘法交换性

如果矩阵类同时实现了数乘的 __matmul____rmatmul__,且结果相同,那么交换性成立。

python 复制代码
class Matrix:
    def __init__(self, data):
        self.data = data

    def __matmul__(self, other):
        if isinstance(other, (int, float)):
            return Matrix([[other * x for x in row] for row in self.data])
        # 其他类型...略
        return NotImplemented

    def __rmatmul__(self, other):
        # 数乘是交换的,所以可以直接返回 self.__matmul__(other)
        return self.__matmul__(other)
     
	  def __repr__(self):
        return '\n'.join([' '.join(map(str, row)) for row in self.data])

注意

这里 __rmatmul__ 委托给了 __matmul__,因为数乘是交换的。但通常不应假设所有情况都交换,比如矩阵乘法就不交换。所以对于矩阵乘法,必须独立实现。

验证:

python 复制代码
M = Matrix([[1, 2], [3, 4]])
print(2 @ M)   # 与 M @ 2 结果相同

运行结果:

复制代码
2 4
6 8

示例 3:支持反向矩阵乘法(非交换)

python 复制代码
class Matrix:
    def __init__(self, data):
        self.data = data
        self.rows = len(data)
        self.cols = len(data[0]) if data else 0

    def __matmul__(self, other):
        if isinstance(other, Matrix):
            if self.cols != other.rows:
                raise ValueError("Incompatible dimensions")
            # 计算 self @ other
            result = [[0 for _ in range(other.cols)] for _ in range(self.rows)]
            for i in range(self.rows):
                for j in range(other.cols):
                    for k in range(self.cols):
                        result[i][j] += self.data[i][k] * other.data[k][j]
            return Matrix(result)
        return NotImplemented

    def __rmatmul__(self, other):
        if isinstance(other, Matrix):
            if other.cols != self.rows:
                raise ValueError("Incompatible dimensions")
            # 计算 other @ self
            result = [[0 for _ in range(self.cols)] for _ in range(other.rows)]
            for i in range(other.rows):
                for j in range(self.cols):
                    for k in range(other.cols):
                        result[i][j] += other.data[i][k] * self.data[k][j]
            return Matrix(result)
        return NotImplemented

    def __repr__(self):
        return '\n'.join([' '.join(map(str, row)) for row in self.data])

为了真正触发 __rmatmul__,我们需要左操作数是不支持矩阵乘法的类型,例如 int。但 int 没有 __matmul__,所以会触发反向。但上面的例子中,两个矩阵相乘时,正向 __matmul__ 已经处理,不会调用反向。因此 __rmatmul__ 主要用于混合类型,如标量与矩阵、自定义类型与内置类型等。

验证:

python 复制代码
A = Matrix([[1, 2], [3, 4]])
B = Matrix([[5, 6], [7, 8]])
print(A @ B)   # 调用 __matmul__
print(B @ A)   # 实际上不触发 __rmatmul__。我们需要混合类型才能触发。

运行结果:

复制代码
19 22
43 50
23 34
31 46

验证:

混合类型触发反向

python 复制代码
M = Matrix([[1, 2], [3, 4]])
scalar = 2
result = scalar @ M   # 触发 M.__rmatmul__(scalar)

运行结果:

复制代码
Traceback (most recent call last):
  File "\py_magicmethods_rmatmul_03.py", line xx, in <module>
    result = scalar @ M   # 触发 M.__rmatmul__(scalar)
             ~~~~~~~^~~
TypeError: unsupported operand type(s) for @: 'int' and 'Matrix'

示例 4:处理其他自定义类型

假设我们有两个自定义类型 MatrixVector,我们希望 Vector 能左乘 Matrix(即向量是行向量?)。此时需要实现 Vector.__matmul__Matrix.__rmatmul__ 配合。

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

    def __matmul__(self, other):
        if isinstance(other, Matrix):
            # 向量是行向量,左乘矩阵:向量长度应等于矩阵行数
            if len(self.data) != other.rows:
                raise ValueError("Incompatible dimensions")
            # 计算行向量 @ 矩阵,结果是一个行向量
            result = [sum(self.data[k] * other.data[k][j] for k in range(other.rows)) for j in range(other.cols)]
            return Vector(result)
        return NotImplemented

class Matrix:
    # ... 同前
    def __rmatmul__(self, other):
        if isinstance(other, Vector):
            # 矩阵右乘向量,这里 other 是左操作数,即 Vector,所以是 other @ self
            # 但 other 是 Vector,我们已经定义了 Vector.__matmul__(Matrix),所以应该调用 other @ self
            # 但这里 __rmatmul__ 被调用时,other 是 Vector,self 是 Matrix。我们可以复用已有的正向逻辑:
            return other @ self   # 这会调用 Vector.__matmul__(Matrix)
        return NotImplemented

最佳实践

对于二元运算,最好成对实现正向和反向方法,以确保所有顺序都能正确处理。

验证:

python 复制代码
v = Vector([1, 2])
M = Matrix([[1, 0], [0, 1]])
result = v @ M   # 正常
result2 = M @ v  # 这应该触发 v.__rmatmul__(M),如果 Matrix.__matmul__(Vector) 不存在,但本例中没有定义 Matrix.__matmul__(Vector),所以会尝试 v.__rmatmul__(M)。但我们没有在 Vector 中定义 __rmatmul__,所以最终失败。因此,如果需要 M @ v,应该定义 Matrix.__matmul__(Vector) 或 Vector.__rmatmul__(M)。

运行结果:

复制代码
Traceback (most recent call last):
  File "\py_magicmethods_rmatmul_04.py", line xx, in <module>
    result2 = M @ v
              ~~^~~
TypeError: unsupported operand type(s) for @: 'Matrix' and 'Vector'

6. 与 __matmul____imatmul__ 的关系

方法 作用 典型返回值 调用时机
__matmul__(self, other) 正向矩阵乘法 self @ other 新对象 x @ y
__rmatmul__(self, other) 反向矩阵乘法 other @ self 新对象 正向返回 NotImplemented
__imatmul__(self, other) 就地矩阵乘法 self @= other self x @= y

关键区别

  • __rmatmul__ 用于左操作数不支持运算时的反向调用,由于矩阵乘法不满足交换律,必须独立实现。
  • __imatmul__ 用于原地修改对象,应返回 self,适用于可变对象。

7. 注意事项与陷阱

  • 不要调用 self @ other :在 __rmatmul__ 中调用 self @ other 会再次触发 __matmul__,可能导致无限递归(如果 __matmul__ 返回 NotImplemented)。应直接实现运算逻辑。
  • 正确处理 NotImplemented :当类型不兼容时返回 NotImplemented,而不是抛出异常 。
  • 维度检查 :对于矩阵乘法,必须检查维度兼容性,不匹配时应抛出 ValueError
  • 与正向方法的一致性 :确保 other @ self 的结果符合数学定义,并与 self @ other 不同(除非运算交换)。
  • 避免滥用 :虽然 __rmatmul__ 可以赋予任何含义,但为了代码可读性,建议遵循矩阵乘法的设计初衷。

8. 总结

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

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

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

相关推荐
lsx2024061 小时前
Django 视图 - FBV 与 CBV
开发语言
不会写DN1 小时前
如何让两个Go程序远程调用?
开发语言·qt·golang
啊哈哈121381 小时前
从零构建 Multi-Agent 系统:SQLAgent + RAGAgent + 智能路由实战
人工智能·python
froginwe112 小时前
MongoDB 关系
开发语言
墨染天姬2 小时前
【AI】PyTorch/TF 也会变成考古?
人工智能·pytorch·python
ん贤4 小时前
Go channel 深入解析
开发语言·后端·golang
小陳参上6 小时前
用Python创建一个Discord聊天机器人
jvm·数据库·python
2301_789015626 小时前
DS进阶:AVL树
开发语言·数据结构·c++·算法
Filotimo_8 小时前
5.3 Internet基础知识
开发语言·php