Как работает Python’s super() с множественным наследованием?
Например, учитывая следующий код:
class First(object):
def __init__(self):
print "first"
class Second(object):
def __init__(self):
print "second"
class Third(First, Second):
def __init__(self):
super(Third, self).__init__()
print "that's it"
Какой метод родительского класса Third ссылается super().__init__? Могу ли я выбрать, какой метод родителя выполняется?
Я знаю, что это как-то связано с порядком разрешения методов (MRO).
Понимание super() и множественного наследования
Функция super() в Python предназначена для вызова методов из родительских классов, особенно полезна в сценариях множественного наследования. При использовании super() Python не просто вызывает метод непосредственного родительского класса - он следует определенному алгоритму, называемому Порядком Разрешения Методов (Method Resolution Order, MRO).
При множественном наследовании класс может наследоваться от нескольких родительских классов, что создает сложность в определении, какой метод вызывать, когда этот метод существует в нескольких родительских классах. MRO обеспечивает последовательный способ разрешения этой неоднозначности.
class First(object):
def __init__(self):
print "first"
class Second(object):
def __init__(self):
print "second"
class Third(First, Second):
def __init__(self):
super(Third, self).__init__() # Это вызовет First.__init__(), а не Second.__init__()
print "that's it"
В этом примере при создании экземпляра Third вывод будет следующим:
first
that's it
Обратите внимание, что second никогда не выводится, потому что super(Third, self).__init__() вызывает First.__init__() вместо Second.__init__().
Порядок Разрешения Методов (MRO) Объяснено
Порядок Разрешения Методов (Method Resolution Order, MRO) - это последовательность, в которой Python ищет методы и атрибуты в иерархии классов. Python использует алгоритм линейизации C3 для определения этого порядка, что обеспечивает последовательный и предсказуемый поиск методов.
Согласно статье в Википедии о множественном наследовании, Python создает список классов с использованием алгоритма линейизации C3 (или Порядка Разрешения Методов (MRO)). Порядок наследования влияет на семантику класса.
Алгоритм линейизации C3 гарантирует:
- Локальный приоритет: родительский класс появляется перед своими потомками в MRO
- Монотонность: если класс появляется в MRO двух разных подклассов, он появляется в том же порядке в обоих MRO
Вы можете проверить MRO любого класса с помощью атрибута __mro__:
print(Third.__mro__)
# Output: (<class '__main__.Third'>, <class '__main__.First'>, <class '__main__.Second'>, <class 'object'>)
MRO для Third - [Third, First, Second, object], что означает:
- Начинаем с самого класса
Third - Затем
First(первый родитель в списке наследования) - Затем
Second(второй родитель в списке наследования) - Наконец
object(базовый класс в Python 3)
Как super() Определяет, Какой Метод Вызвать
При вызове super().__init__() Python выполняет следующие шаги:
-
Определение текущего класса и экземпляра:
super(Third, self)указывает Python, что мы начинаем поиск с классаThirdдля экземпляраself -
Поиск следующего класса в MRO: Python смотрит на MRO и находит класс, следующий за
Thirdв последовательности -
Вызов метода в этом классе: Python вызывает метод
__init__в следующем классе в MRO
В вашем примере:
- MRO:
[Third, First, Second, object] super(Third, self).__init__()начинается сThirdи ищет следующий класс- Следующий класс -
First, поэтому вызываетсяFirst.__init__()
Это поведение последовательное и предсказуемое, что делает использование super() безопасным в сложных иерархиях наследования.
class A(object):
def method(self):
print "A"
class B(A):
def method(self):
super(B, self).method()
print "B"
class C(A):
def method(self):
super(C, self).method()
print "C"
class D(B, C):
def method(self):
super(D, self).method()
print "D"
# Output при вызове D().method():
# A
# C
# B
# D
MRO для D - [D, B, C, A, object], поэтому каждый вызов super().method() переходит к следующему классу в этой последовательности.
Практические Примеры и Лучшие Практики
Шаблон Алмазного Наследования
Распространенный шаблон при множественном наследовании - это алмазный шаблон, когда два класса наследуются от одного базового класса:
class Base(object):
def __init__(self):
print "Base"
class Left(Base):
def __init__(self):
super(Left, self).__init__()
print "Left"
class Right(Base):
def __init__(self):
super(Right, self).__init__()
print "Right"
class Diamond(Left, Right):
def __init__(self):
super(Diamond, self).__init__()
print "Diamond"
# Output при создании Diamond():
# Base
# Right
# Left
# Diamond
MRO для Diamond - [Diamond, Left, Right, Base, object], поэтому каждый вызов super().__init__() вызывает следующий класс в этой последовательности.
Цепочка Методов с super()
Правильный способ использования super() при множественном наследовании - всегда вызывать его и позволить ему обрабатывать разрешение метода:
class Animal(object):
def __init__(self):
print "Animal init"
class Mammal(Animal):
def __init__(self):
super(Mammal, self).__init__()
print "Mammal init"
class Winged(object):
def __init__(self):
super(Winged, self).__init__()
print "Winged init"
class Bat(Mammal, Winged):
def __init__(self):
super(Bat, self).__init__()
print "Bat init"
# Output:
# Animal init
# Winged init
# Mammal init
# Bat init
Этот подход гарантирует, что все методы инициализации родительских классов вызываются в правильном порядке, следуя MRO.
Расширенные Сценарии и Крайние Случаи
Сложное Множественное Наследование
При работе с более сложными иерархиями наследования понимание MRO становится критически важным:
class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass
class E(C, A): pass
class F(D, E): pass
print(F.__mro__)
# Output: (<class '__main__.F'>, <class '__main__.D'>, <class '__main__.E'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
Алгоритм C3 гарантирует, что MRO последовательный и избегает проблемы “смертельного алмаза смерти” (deadly diamond of death).
super() с Различными Аргументами
super() можно использовать с различными сигнатурами методов, но нужно быть осторожным с передачей аргументов:
class Parent(object):
def __init__(self, x, y):
self.x = x
self.y = y
print "Parent init"
class Child(Parent):
def __init__(self, x, y, z):
super(Child, self).__init__(x, y) # Передаем только аргументы, которые ожидает Parent
self.z = z
print "Child init"
Явное Управление Вызовом Методов
Хотя super() следует MRO, вы можете выбрать, какой метод родительского класса вызывать явно:
Прямые Вызовы Методов
Чтобы вызвать метод конкретного родительского класса, вы можете вызвать его напрямую:
class First(object):
def __init__(self):
print "first"
class Second(object):
def __init__(self):
print "second"
class Third(First, Second):
def __init__(self):
# Явный вызов First.__init__()
First.__init__(self)
print "that's it"
# Output:
# first
# that's it
Вызов Любого Конкретного Родителя
Вы можете вызывать метод любого родительского класса независимо от порядка наследования:
class Third(First, Second):
def __init__(self):
# Вызов Second.__init__() даже несмотря на то, что First идет первым в наследовании
Second.__init__(self)
print "that's it"
# Output:
# second
# that's it
Смешанные Подходы
Вы можете комбинировать super() с прямыми вызовами для более сложных сценариев:
class First(object):
def method(self):
print "first"
class Second(object):
def method(self):
print "second"
class Third(First, Second):
def method(self):
super(Third, self).method() # Вызывает First.method()
Second.method(self) # Явно вызывает Second.method()
print "that's it"
# Output:
# first
# second
# that's it
Однако будьте осторожны при смешивании подходов, так как это может привести к многократным вызовам методов или вызовам в неожиданном порядке, потенциально вызывая ошибки.
Источники
-
Mastering Python Inheritance: Using Parent Methods In Child Classes | ShunChild
-
Python for AI: Week 8-Classes in Python: 3. Inheritance and Polymorphism | Medium
Заключение
-
super() следует MRO: Функция
super()в Python использует Порядок Разрешения Методов (MRO), определяемый алгоритмом линейизации C3, для решения, какой метод родительского класса вызывать. -
В вашем примере:
super(Third, self).__init__()вызываетFirst.__init__()потому, что MRO -[Third, First, Second, object], что делаетFirstследующим классом в последовательности послеThird. -
Выбор методов родительских классов: Хотя вы не можете изменить порядок MRO, вы можете явно вызывать любой метод родительского класса, ссылаясь на него напрямую (например,
Second.__init__(self)). -
Лучшие практики: Последовательно используйте
super()в иерархиях наследования для обеспечения правильной цепочки вызовов методов и избежания дублирующих вызовов. Используйте прямые вызовы методов только тогда, когда вам явно нужно обойти MRO. -
Проверка MRO: Всегда проверяйте атрибут
__mro__ваших классов, чтобы понять порядок поиска методов, особенно при работе со сложными сценариями множественного наследования.