Наследование, MRO и магические методы

Продвинутый ООП-вопрос: множественное наследование, порядок MRO и ключевые dunder-методы.

MRO (Method Resolution Order) — порядок, в котором Python ищет метод по цепочке базовых классов. Строится алгоритмом C3 и доступен как Class.__mro__.

Вопрос: какой метод вызовется при множественном наследовании?

Чёткий ответ. Python ищет метод по списку MRO слева направо. Для «ромбовидного» наследования C3-алгоритм гарантирует, что каждый класс встретится один раз и в логичном порядке.

class A:
    def who(self): return "A"
class B(A):
    def who(self): return "B"
class C(A):
    def who(self): return "C"
class D(B, C):
    pass

print(D().who())                            # берётся из B (раньше в MRO)
print([cls.__name__ for cls in D.__mro__])

Вывод:

B
['D', 'B', 'C', 'A', 'object']

MRO для D: сам D, затем B, C, общий предок A и в конце object. Метод who берётся из B, потому что он раньше в списке.

Магические методы: __str__, __eq__, __hash__

Dunder-методы (double underscore) встраивают объект в язык. __str__ — человекочитаемое представление, __eq__ — сравнение через ==, __hash__ — чтобы объект можно было класть в множество и использовать ключом.

class Point:
    def __init__(self, x, y):
        self.x, self.y = x, y

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

    def __eq__(self, other):
        return (self.x, self.y) == (other.x, other.y)

    def __hash__(self):
        return hash((self.x, self.y))

print(str(Point(1, 2)))
print(Point(1, 2) == Point(1, 2))           # __eq__
points = {Point(1, 2), Point(1, 2), Point(3, 4)}
print("уникальных точек:", len(points))     # дубликат схлопнулся

Вывод:

(1, 2)
True
уникальных точек: 2

Важное правило про __eq__ и __hash__

Если переопределяете __eq__, переопределяйте и __hash__ — иначе объект станет нехешируемым и не попадёт в множество. Равные объекты обязаны иметь одинаковый хеш.

class Money:
    def __init__(self, amount):
        self.amount = amount
    def __eq__(self, other):
        return self.amount == other.amount
    def __hash__(self):
        return hash(self.amount)

print(Money(100) == Money(100))
print(hash(Money(100)) == hash(Money(100)))

Вывод:

True
True

Итог

  • MRO задаёт порядок поиска метода; смотрите его через Class.__mro__.
  • __str__ — печать, __eq__ — сравнение, __hash__ — для множеств и ключей словаря.
  • Переопределили __eq__ — переопределите и __hash__; равные объекты имеют равный хеш.
Проверьте себя
1. Что задаёт MRO?
AСкорость методов
BПорядок поиска метода по цепочке базовых классов
CПамять объекта
DСписок атрибутов
2. Зачем нужен __hash__?
AДля печати объекта
BЧтобы объект можно было класть в set и использовать ключом словаря
CДля сравнения через <
DДля копирования
3. Что нужно сделать, если переопределили __eq__?
AНичего
BТакже переопределить __hash__
CУдалить __init__
DДобавить __new__
Поддержать проект