Вызов родительского класса __init__ с множественным наследованием, как правильно?
Допустим, у меня сценарий множественного наследования:
class A(object):
# code for A here
class B(object):
# code for B here
class C(A, B):
def __init__(self):
# What's the right code to write here to ensure
# A.__init__ and B.__init__ get called?
Существует два типичных подхода к написанию C
__init__
:
ParentClass.__init__(self)
super(DerivedClass, self).__init__()
Однако в любом случае, если родительские классы (A
и B
) не следуют одному и тому же соглашению, то код не будет работать корректно (некоторые могут быть пропущены или вызываться несколько раз).
Итак, каков еще раз правильный способ? Легко сказать "просто будьте последовательны, следуйте тому или иному", но если A
или B
взяты из библиотеки стороннего разработчика, что тогда? Существует ли подход, который может гарантировать, что все конструкторы родительского класса будут вызваны (и в правильном порядке, и только один раз)?
Редактировать: чтобы понять, что я имею в виду, если я делаю:
class A(object):
def __init__(self):
print("Entering A")
super(A, self).__init__()
print("Leaving A")
class B(object):
def __init__(self):
print("Entering B")
super(B, self).__init__()
print("Leaving B")
class C(A, B):
def __init__(self):
print("Entering C")
A.__init__(self)
B.__init__(self)
print("Leaving C")
Тогда я получаю:
Entering C
Entering A
Entering B
Leaving B
Leaving A
Entering B
Leaving B
Leaving C
Обратите внимание, что B
инициализация вызывается дважды. Если я сделаю:
class A(object):
def __init__(self):
print("Entering A")
print("Leaving A")
class B(object):
def __init__(self):
print("Entering B")
super(B, self).__init__()
print("Leaving B")
class C(A, B):
def __init__(self):
print("Entering C")
super(C, self).__init__()
print("Leaving C")
Тогда я получаю:
Entering C
Entering A
Leaving A
Leaving C
Обратите внимание, что B
init никогда не вызывается. Итак, кажется, что если я не знаю / не контролирую инициализации классов, от которых я наследую (A
и B
), я не могу сделать безопасный выбор для класса, который я пишу (C
).
Переведено автоматически
Ответ 1
Ответ на ваш вопрос зависит от одного очень важного аспекта: предназначены ли ваши базовые классы для множественного наследования?
Существует 3 разных сценария:
If your base classes are separate entities that are capable of functioning independently and they don't know each other, they're not designed for multiple inheritance. Example:
class Foo:
def __init__(self):
self.foo = 'foo'
class Bar:
def __init__(self, bar):
self.bar = bar
Важно: Обратите внимание, что ни Foo
ни Bar
не вызывает super().__init__()
! Вот почему ваш код работал некорректно. Из-за способа, которым работает алмазное наследование в python, классы, базовым классом которых является object
, не должны вызывать super().__init__()
. Как вы заметили, это приведет к нарушению множественного наследования, потому что в конечном итоге вы вызовете другой класс __init__
, а неobject.__init__()
. (Отказ от ответственности: Избегать super().__init__()
в object
подклассах - это моя личная рекомендация и ни в коем случае не согласованный консенсус в сообществе python. Некоторые люди предпочитают использовать super
в каждом классе, утверждая, что вы всегда можете написать адаптер, если класс ведет себя не так, как вы ожидаете.)
Это также означает, что вам никогда не следует писать класс, который наследует от object
и не имеет __init__
метода. Отсутствие определения __init__
метода вообще имеет тот же эффект, что и вызов super().__init__()
. Если ваш класс наследуется напрямую от object
, обязательно добавьте пустой конструктор следующим образом:
class Base(object):
def __init__(self):
pass
В любом случае, в этой ситуации вам придется вызывать каждый родительский конструктор вручную. Есть два способа сделать это:
Без super
class FooBar(Foo, Bar):
def __init__(self, bar='bar'):
Foo.__init__(self) # explicit calls without super
Bar.__init__(self, bar)
С помощью super
class FooBar(Foo, Bar):
def __init__(self, bar='bar'):
super().__init__() # this calls all constructors up to Foo
super(Foo, self).__init__(bar) # this calls all constructors after Foo up
# to Bar
Каждый из этих двух методов имеет свои преимущества и недостатки. Если вы используете super
, ваш класс будет поддерживать внедрение зависимостей. С другой стороны, так легче допустить ошибку. Например, если вы измените порядок Foo
и Bar
(например, class FooBar(Bar, Foo)
), вам придется обновить super
вызовы, чтобы они соответствовали. Без super
вам не нужно беспокоиться об этом, и код намного удобочитаем.
mixin - это класс, который разработан для использования с множественным наследованием. Это означает, что нам не нужно вызывать оба родительских конструктора вручную, потому что mixin автоматически вызовет для нас второй конструктор. Поскольку на этот раз нам нужно вызвать только один конструктор, мы можем сделать это с помощью super
, чтобы избежать необходимости жестко кодировать имя родительского класса.
Пример:
class FooMixin:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) # forwards all unused arguments
self.foo = 'foo'
class Bar:
def __init__(self, bar):
self.bar = bar
class FooBar(FooMixin, Bar):
def __init__(self, bar='bar'):
super().__init__(bar) # a single call is enough to invoke
# all parent constructors
# NOTE: `FooMixin.__init__(self, bar)` would also work, but isn't
# recommended because we don't want to hard-code the parent class.
Важные детали здесь следующие:
super().__init__()
и передает любые аргументы, которые он получает.class FooBar(FooMixin, Bar)
. Если порядок базовых классов неправильный, конструктор mixin никогда не будет вызван.Классы, предназначенные для совместного наследования, во многом похожи на миксины: они передают все неиспользуемые аргументы следующему классу. Как и раньше, нам просто нужно вызвать super().__init__()
и все родительские конструкторы будут вызываться по цепочке.
Пример:
class CoopFoo:
def __init__(self, **kwargs):
super().__init__(**kwargs) # forwards all unused arguments
self.foo = 'foo'
class CoopBar:
def __init__(self, bar, **kwargs):
super().__init__(**kwargs) # forwards all unused arguments
self.bar = bar
class CoopFooBar(CoopFoo, CoopBar):
def __init__(self, bar='bar'):
super().__init__(bar=bar) # pass all arguments on as keyword
# arguments to avoid problems with
# positional arguments and the order
# of the parent classes
В этом случае порядок родительских классов не имеет значения. С таким же успехом мы могли бы сначала наследовать от CoopBar
, и код все равно работал бы так же. Но это верно только потому, что все аргументы передаются как аргументы ключевого слова. Использование позиционных аргументов упростило бы неправильный порядок аргументов, поэтому для кооперативных классов принято принимать только аргументы ключевого слова.
Это также исключение из правила, о котором я упоминал ранее: оба CoopFoo
и CoopBar
наследуются от object
, но они все равно вызывают super().__init__()
. Если бы они этого не сделали, совместного наследования не было бы.
Итог: правильная реализация зависит от классов, от которых вы наследуете.
Конструктор является частью открытого интерфейса класса. Если класс разработан как миксин или для совместного наследования, это должно быть задокументировано. Если в документах ничего подобного не упоминается, можно с уверенностью предположить, что класс не предназначен для совместного множественного наследования.
Ответ 2
Оба способа работают нормально. Подход с использованием super()
обеспечивает большую гибкость для подклассов.
При подходе прямого вызова C.__init__
можно вызывать оба A.__init__
и B.__init__
.
При использовании super()
классы должны быть разработаны для совместного множественного наследования where C
вызывает super
, который вызывает A
код, который также будет вызывать super
который вызывает B
код. Смотрите http://rhettinger.wordpress.com/2011/05/26/super-considered-super подробнее о том, что можно сделать с помощью super
.
[Ответный вопрос отредактирован позже]
Итак, кажется, что если я не знаю / не контролирую инициализации классов, от которых я наследую (A и B), я не могу сделать безопасный выбор для класса, который я пишу (C).
В указанной статье показано, как справиться с этой ситуацией, добавив класс-оболочку вокруг A
и B
. В разделе, озаглавленном "Как включить некооперативный класс", есть проработанный пример.
Можно было бы пожелать, чтобы множественное наследование было проще, позволяя вам без особых усилий создавать классы Car и Airplane для получения FlyingCar, но реальность такова, что отдельно разработанным компонентам часто требуются адаптеры или оболочки, прежде чем они будут сочетаться так легко, как хотелось бы :-)
Еще одна мысль: если вы недовольны составлением функциональности с использованием множественного наследования, вы можете использовать composition для полного контроля над тем, какие методы вызываются в каких случаях.
Ответ 3
Любой подход ("новый стиль" или "старый стиль") будет работать, если у вас есть контроль над исходным кодом для A
и B
. В противном случае может потребоваться использование класса адаптера.
class A(object):
def __init__(self):
print("-> A")
super(A, self).__init__()
print("<- A")
class B(object):
def __init__(self):
print("-> B")
super(B, self).__init__()
print("<- B")
class C(A, B):
def __init__(self):
print("-> C")
# Use super here, instead of explicit calls to __init__
super(C, self).__init__()
print("<- C")
>>> C()
-> C
-> A
-> B
<- B
<- A
<- C
Здесь порядок разрешения метода (MRO) диктует следующее:
C(A, B)
сначала диктует A
, потом B
. MRO - это C -> A -> B -> object
.super(A, self).__init__()
продолжается по цепочке MRO, инициированной в C.__init__
to B.__init__
.super(B, self).__init__()
продолжается по цепочке MRO, инициированной в C.__init__
to object.__init__
.Можно сказать, что этот случай предназначен для множественного наследования.
class A(object):
def __init__(self):
print("-> A")
print("<- A")
class B(object):
def __init__(self):
print("-> B")
# Don't use super here.
print("<- B")
class C(A, B):
def __init__(self):
print("-> C")
A.__init__(self)
B.__init__(self)
print("<- C")
>>> C()
-> C
-> A
<- A
-> B
<- B
<- C
Здесь MRO не имеет значения, поскольку A.__init__
и B.__init__
вызываются явно. class C(B, A):
сработало бы так же хорошо.
Хотя этот случай не "предназначен" для множественного наследования в новом стиле, как предыдущий, множественное наследование все еще возможно.
Теперь, что, если A
и B
взяты из сторонней библиотеки - т. Е. У вас нет контроля над исходным кодом для A
и B
? Краткий ответ: вы должны разработать класс-адаптер, который реализует необходимые super
вызовы, затем использовать пустой класс для определения MRO (см. Статью Раймонда Хеттингера о super
, особенно раздел "Как включить класс, не связанный с взаимодействием").
A
не реализует super
; B
выполняетclass A(object):
def __init__(self):
print("-> A")
print("<- A")
class B(object):
def __init__(self):
print("-> B")
super(B, self).__init__()
print("<- B")
class Adapter(object):
def __init__(self):
print("-> C")
A.__init__(self)
super(Adapter, self).__init__()
print("<- C")
class C(Adapter, B):
pass
>>> C()
-> C
-> A
<- A
-> B
<- B
<- C
Класс Adapter
реализует super
так, что C
может определять MRO, который вступает в действие при super(Adapter, self).__init__()
выполнении.
А что, если все наоборот?
A
реализует super
; B
не выполняетclass A(object):
def __init__(self):
print("-> A")
super(A, self).__init__()
print("<- A")
class B(object):
def __init__(self):
print("-> B")
print("<- B")
class Adapter(object):
def __init__(self):
print("-> C")
super(Adapter, self).__init__()
B.__init__(self)
print("<- C")
class C(Adapter, A):
pass
>>> C()
-> C
-> A
<- A
-> B
<- B
<- C
Здесь тот же шаблон, за исключением того, что изменен порядок выполнения Adapter.__init__
; super
сначала вызов, затем явный вызов. Обратите внимание, что для каждого случая со сторонними родителями требуется уникальный класс адаптера.
Итак, кажется, что если я не знаю / не контролирую инициализации классов, от которых я наследую (
A
иB
), я не могу сделать безопасный выбор для класса, который я пишу (C
).
Хотя вы можете справиться со случаями, когда вы не контролируете исходный код A
и B
с помощью класса адаптера, это правда, что вы должны знать, как реализуются инициализации родительских классов super
(если реализуются вообще), чтобы сделать это.
Ответ 4
Как сказал Рэймонд в своем ответе, прямой вызов A.__init__
и B.__init__
работает нормально, и ваш код будет читабелен.
Однако он не использует ссылку наследования между C
и этими классами. Использование этой ссылки обеспечивает большую согласованность и упрощает конечный рефакторинг и снижает вероятность ошибок. Пример того, как это сделать:
class C(A, B):
def __init__(self):
print("entering c")
for base_class in C.__bases__: # (A, B)
base_class.__init__(self)
print("leaving c")