Вызов родительского класса __init__ с множественным наследованием, какой правильный путь?

Скажем, у меня есть сценарий множественного наследования:

class A(object):
    # code for A here

class B(object):
    # code for B here

class C(A, B):
    def __init__(self):
        # What 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 init вызывается дважды. Если я сделаю:

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

Оба способа работают нормально. Подход с использованием super() приводит к большей гибкости для подклассов.

В подходе прямого вызова C.__init__ может вызывать как A.__init__, так и B.__init__.

При использовании super() классы должны быть разработаны для совместного множественного наследования, где C вызывает super, который вызывает код A, который также вызывает super, который вызывает код B. Подробнее см. http://rhettinger.wordpress.com/2011/05/26/super-considered-super о том, что можно сделать с помощью super.

[Ответ на вопрос позже отредактирован]

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

В приведенной статье показано, как справиться с этой ситуацией, добавив класс оболочки вокруг A и B. В разделе под названием "Как включить некоммерческий класс" существует разработанный пример.

Можно было бы пожелать, чтобы множественное наследование было проще, позволяя вам легко составлять классы Car and Airplane для получения FlyingCar, но реальность такова, что отдельно разработанные компоненты часто нуждаются в адаптерах или обертках, прежде чем объединиться так же легко, как хотелось бы:-)

Еще одна мысль: если вы недовольны созданием функциональности с использованием множественного наследования, вы можете использовать композицию для полного контроля над тем, какие методы вызывают в тех случаях.

Ответ 2

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

Есть 3 разных сценария:

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

    Если ваши базовые классы являются отдельными объектами, которые способны функционировать независимо и не знают друг друга, они не предназначены для множественного наследования. Пример:

    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 -subclasses - это моя личная рекомендация и ни в коем случае не согласованный консенсус в сообществе 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 - это класс, предназначенный для множественного наследования. Это означает, что нам не нужно вызывать оба родительских конструктора вручную, потому что миксин автоматически вызовет 2-й конструктор для нас. Поскольку на этот раз нам нужно вызывать только один конструктор, мы можем сделать это с помощью 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): 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__(). Если бы они этого не сделали, кооперативного наследства не было бы.

Итог: правильная реализация зависит от классов, от которых вы наследуете.

Конструктор является частью открытого интерфейса класса. Если класс задуман как миксин или для совместного наследования, это должно быть задокументировано. Если в документах не упоминается ничего подобного, можно предположить, что класс не предназначен для совместного множественного наследования.

Ответ 3

Эта статья помогает объяснить совместное множественное наследование:

http://www.artima.com/weblogs/viewpost.jsp?thread=281127

В нем упоминается полезный метод mro(), который показывает вам порядок разрешения метода. Во втором примере, где вы вызываете super в A, вызов super продолжается в MRO. Следующий класс в порядке B, поэтому B init вызывается в первый раз.

Здесь более техническая статья с официального сайта python:

http://www.python.org/download/releases/2.3/mro/

Ответ 4

Если вы умножаете классы подкласса из сторонних библиотек, то нет, нет никакого слепого подхода к вызову методов базового класса __init__ (или любых других методов), который фактически работает независимо от того, как программируются базовые классы.

super позволяет писать классы, предназначенные для совместного использования методов в составе сложных кратных деревьев наследования, которые не должны быть известны автору класса. Но нет способа использовать его для правильного наследования от произвольных классов, которые могут или не могут использовать super.

По существу, независимо от того, должен ли класс быть подклассифицирован с помощью super или с прямыми вызовами базового класса, это свойство, которое является частью открытого "класса" класса, и должно быть документировано как таковое. Если вы используете сторонние библиотеки так, как ожидал автор библиотеки, и библиотека имела разумную документацию, она обычно расскажет вам, что вам нужно делать для подкласса определенных вещей. Если нет, то вам придется посмотреть исходный код для классов, которые вы подписали, и посмотреть, каково их соглашение об использовании базового класса. Если вы комбинируете несколько классов из одной или нескольких сторонних библиотек таким образом, что авторы библиотеки не ожидали, возможно, не удастся последовательно вызвать методы суперкласса; если класс A является частью иерархии с использованием super, а класс B является частью иерархии, которая не использует супер, то ни один из параметров не будет работать. Вам нужно будет найти стратегию, которая будет работать для каждого конкретного случая.

Ответ 5

Как сказал Раймонд в своем ответе, прямой вызов 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")

Ответ 6

Любой подход ("новый стиль" или "старый стиль") будет работать, если у вас есть контроль над исходным кодом для 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__ до B.__init__.
  • super(B, self).__init__() продолжается по цепочке MRO, инициированной в C.__init__ для 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

Class 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 (если вообще), чтобы сделать так.