Python MRO для операторов: выбирает RHS `__rmul__` вместо LHS `__mul__`, если RHS является подклассом

Рассмотрим следующий автономный пример:

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. Если они не связаны между собой, то результат будет ожидаемым.

Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
0
1
50
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Это как раз из-за наследственности. Порядок операций будет другим, если правый операнд является подклассом левого операнда. Это позволяет подклассу переопределять поведение суперкласса. Давайте посмотрим это в действии на нескольких примерах. Сначала указан обычный порядок операций, затем несколько сложных примеров и, наконец, пример подкласса:

Пример 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__(). В этом вся разница). Этот код проходит три этапа:

  • Шаг 1: MyInt1.__mul__() называется MyInt1.__mul__(a, other).
  • Шаг 2: Поскольку строка return равна self.val * other, вызывается MyInt2.__rmul__(). Это потому, что other — это объект MyInt2, а не целое число, как в первом коде. MyInt2.__rmul__() называется MyInt2.__rmul__(b, other).
  • Шаг 3: Поскольку 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 наименьшего неожиданности...)

geometrian 12.08.2024 03:06

Другие вопросы по теме