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 时,解释器会:
- 调用
x.__matmul__(y)(C 层对应nb_matrix_multiply)。 - 如果返回
NotImplemented,则调用y.__rmatmul__(x)(C 层通过交换参数再次调用nb_matrix_multiply,但 Python 会自动将这次调用定向到__rmatmul__)。 - 如果仍返回
NotImplemented,抛出TypeError。
这种机制确保了非交换运算的正确性。如果直接使用 y.__matmul__(x) 代替反向调用,结果将是错误的,因为 x @ y 和 y @ x 语义不同 。因此,__rmatmul__ 是专门为右操作数设计的独立方法。
4. 设计原则与最佳实践
- 独立实现 :由于矩阵乘法不满足交换律,
__rmatmul__必须实现other @ self的语义,不能简单地委托给__matmul__(除非你明确知道二者等价,比如标量与矩阵的乘法通常可交换,但即使如此也应独立实现以保证清晰性)。 - 类型检查 :应检查
other的类型是否兼容。如果类型不匹配,返回NotImplemented。不要抛出异常,这样给其他类型机会处理。 - 返回新对象 :矩阵乘法通常应返回新对象,不修改原操作数(除非类是可变的且你使用
@=运算符)。 - 遵循数学规则 :对于矩阵乘法,必须检查维度兼容性(如果适用),否则应抛出
ValueError。对于标量与矩阵的乘法,通常定义为数乘(每个元素乘以标量),这可以看作是一种交换运算。 - 避免无限递归 :在
__rmatmul__中不要调用self @ other或other @ 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:处理其他自定义类型
假设我们有两个自定义类型 Matrix 和 Vector,我们希望 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 运算符生态无缝集成的关键,尤其是在处理混合类型运算时。通过理解其底层机制和设计原则,你可以让类支持更自然的矩阵乘法语义,提升代码的可读性和可用性。
如果在学习过程中遇到问题,欢迎在评论区留言讨论!