Рассмотрим следующий автономный пример:
class Matrix:
def __mul__(self, other):
print("Matrix.__mul__(⋯)")
return NotImplemented
def __rmul__(self, other):
print("Matrix.__rmul__(⋯)")
return NotImplemented
class Vector(Matrix):
def __mul__(self, other):
print("Vector.__mul__(⋯)")
return NotImplemented
def __rmul__(self, other):
print("Vector.__rmul__(⋯)")
return NotImplemented
matr = Matrix()
vec = Vector()
print("=== Using explicit `__mul__`: == = ")
matr.__mul__(vec)
print()
print("=== Using implicit `*`: == = ")
matr * vec
с выводом (CPython 3.12.2):
=== Using explicit `__mul__`: ===
Matrix.__mul__(⋯)
=== Using implicit `*`: ===
Vector.__rmul__(⋯)
Matrix.__mul__(⋯)
(TypeError raised: "unsupported operand type(s) for *: 'Matrix' and 'Vector'")
Я пытаюсь понять, почему во втором случае Vector.__rmul__
вызывается раньше Matrix.__mul__
.
Насколько я понимаю, Python видит *
, затем смотрит на левую и правую стороны. Если он видит, что у LHS есть __mul__
, он вызывается (с аргументами self=LHS, right=RHS). Если это возвращает NotImplemented
, только тогда он пробует RHS __rmul__
(с аргументами self=RHS, right=LHS).
В частности, в этом случае, когда он смотрит на LHS, он должен найти Matrix.__mul__
, и только когда это не удастся, он должен попытаться Vector.__rmul__
. Но он делает это наоборот! Почему?
Также важно отметить: это происходит только тогда, когда Vector
является подклассом Matrix
. Если они не связаны между собой, то результат будет ожидаемым.
Это как раз из-за наследственности. Порядок операций будет другим, если правый операнд является подклассом левого операнда. Это позволяет подклассу переопределять поведение суперкласса. Давайте посмотрим это в действии на нескольких примерах. Сначала указан обычный порядок операций, затем несколько сложных примеров и, наконец, пример подкласса:
Пример 1: Обычный порядок работы №1
class MyInt1():
def __init__(self, val=0):
self.val = val
def __int__(self):
return self.val
def __mul__(self, other):
print('in MyInt1.__mul__()')
return self.val * other.val
class MyInt2():
def __init__(self, val=0):
self.val = val
def __rmul__(self, other):
print('in MyInt2.__rmul__()')
return other * self.val
if __name__ == '__main__':
a = MyInt1(1)
b = MyInt2(3)
c = a * b
Выходы:
in MyInt1.__mul__()
В приведенном выше коде класс MyInt1
определяет метод __mul__()
, который возвращает произведение self.val
и other.val
. Обратите внимание, что MyInt2
__rmul__()
никогда не вызывался, поскольку умножение уже выполнялось и разрешалось до достижения этого шага. Вот еще один код:
class MyInt1():
def __init__(self, val=0):
self.val = val
def __int__(self):
return self.val
def __mul__(self, other):
print('in MyInt1.__mul__()')
return self.val * other
class MyInt2():
def __init__(self, val=0):
self.val = val
def __rmul__(self, other):
print('in MyInt2.__rmul__()')
return other * self.val
if __name__ == '__main__':
a = MyInt1(1)
b = MyInt2(3)
c = a * b
Выходы:
in MyInt1.__mul__()
in MyInt2.__rmul__()
Этот код, хотя и очень похож на приведенный выше, на самом деле совершенно другой (обратите внимание на self.val * other
вместо self.val * other.val
в MyInt1.__mul__()
. В этом вся разница). Этот код проходит три этапа:
MyInt1.__mul__()
называется MyInt1.__mul__(a, other)
.return
равна self.val * other
, вызывается MyInt2.__rmul__()
. Это потому, что other
— это объект MyInt2
, а не целое число, как в первом коде. MyInt2.__rmul__()
называется MyInt2.__rmul__(b, other)
.other
в данном случае — это a.val
, это int
, и, следовательно, это приводит к a.val * b.val
, что равно 3.Теперь давайте посмотрим на другой код, который, хотя и не имеет особого смысла, но демонстрирует концепцию более четко:
class MyInt1():
def __mul__(self, other):
print('in MyInt1.__mul__()')
return 2 * 2
def __rmul__(self, other):
print('in MyInt1.__rmul__()')
return 4 * 4
class MyInt2():
def __mul__(self, other):
print('in MyInt2.__mul__()')
return 3 * 3
def __rmul__(self, other):
print('in MyInt2.__rmul__()')
return 6 * 6
if __name__ == '__main__':
a = MyInt1()
b = MyInt2()
c = a * b
print(c)
Выходы:
in MyInt1.__mul__()
4
Этот код — еще один пример нормального порядка операций. Теперь давайте посмотрим на этот код, но с включением наследования:
class MyInt1():
def __mul__(self, other):
print('in MyInt1.__mul__()')
return 2 * 2
def __rmul__(self, other):
print('in MyInt1.__rmul__()')
return 4 * 4
class MyInt2(MyInt1):
def __mul__(self, other):
print('in MyInt2.__mul__()')
return 3 * 3
def __rmul__(self, other):
print('in MyInt2.__rmul__()')
return 6 * 6
if __name__ == '__main__':
a = MyInt1()
b = MyInt2()
c = a * b
d = b * a
print(c)
print(d)
Выходы:
in MyInt2.__rmul__()
36
in MyInt2.__mul__()
9
Как вы можете видеть в этом случае, то, является ли подкласс левым или правым операндом, больше не имеет значения. В обоих случаях вызываются методы подкласса. Если подкласс является левым операндом, то нет большой разницы с обычным порядком, поскольку левый вызывается раньше правого. Однако в случае с правильным операндом предпочтение по-прежнему отдается подклассу, то есть правильному операнду. Такое поведение позволяет подклассам переопределять поведение своих суперклассов.
Ссылка: Дополнительную информацию см. в специальном порядке предпочтения __add__ и __radd__.
Хотя я считаю, что ваши примеры следует упростить, это отвечает на вопрос, и я ценю, что помимо этого вы попытались понять, почему язык делает это. (Хотя, спросите вы меня, нарушение обычного наследования методов для особого случая нарушает якобы почитаемый принцип Python наименьшего неожиданности...)