Вопрос-Ответ

Calling parent class __init__ with multiple inheritance, what's the right way?

Вызов родительского класса __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__:


  1. (старый стиль) ParentClass.__init__(self)

  2. (в более новом стиле) 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

Обратите внимание, что Binit никогда не вызывается. Итак, кажется, что если я не знаю / не контролирую инициализации классов, от которых я наследую (A и B), я не могу сделать безопасный выбор для класса, который я пишу (C).

Переведено автоматически
Ответ 1

Ответ на ваш вопрос зависит от одного очень важного аспекта: предназначены ли ваши базовые классы для множественного наследования?

Существует 3 разных сценария:


  1. Базовые классы являются несвязанными, автономными классами.


    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 вам не нужно беспокоиться об этом, и код намного удобочитаем.


  2. Один из классов - это mixin .


    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.

    Важные детали здесь следующие:



    • Mixin вызывает super().__init__() и передает любые аргументы, которые он получает.

    • Подкласс наследуется от mixin первым: class FooBar(FooMixin, Bar). Если порядок базовых классов неправильный, конструктор mixin никогда не будет вызван.


  3. Все базовые классы предназначены для совместного наследования.


    Классы, предназначенные для совместного наследования, во многом похожи на миксины: они передают все неиспользуемые аргументы следующему классу. Как и раньше, нам просто нужно вызвать 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")
python