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

175

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

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«s __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

Обратите внимание, что Binit вызывается дважды. Если я сделаю:

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 никогда не вызывается. Таким образом, кажется, что если я не знаю / не контролирую init классов, которые я наследую ( Aи B), я не могу сделать безопасный выбор для класса, который я пишу ( C).

Адам Паркин
источник

Ответы:

78

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

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

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

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

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

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

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

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

Раймонд Хеттингер
источник
4
Нет, они не Если инициализация B не вызывает super, то инициализация B не будет вызвана, если мы сделаем super().__init__()подход. Если я звоню A.__init__()и B.__init__()напрямую, то (если A и B звонят super), я получаю инициат B, вызываемый несколько раз.
Адам Паркин
3
@AdamParkin (относительно вашего вопроса в отредактированном виде): если один из родительских классов не предназначен для использования с super () , его обычно можно обернуть таким образом, чтобы добавить супер- вызов. Ссылочная статья показывает отработанный пример в разделе под названием «Как включить некооперативный класс».
Раймонд Хеттингер
1
Каким-то образом мне удалось пропустить этот раздел, когда я прочитал статью. Именно то, что я искал. Спасибо!
Адам Паркин
1
Если вы пишете на python (надеюсь, 3!) И используете наследование любого рода, но особенно многократного, тогда rhettinger.wordpress.com/2011/05/26/super-considered-super необходимо прочитать.
Шон Механ,
1
Проголосование, потому что мы наконец-то знаем, почему у нас нет летающих машин, когда мы были уверены, что у нас уже есть.
Msout
66

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

Есть 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 моя личная рекомендация и отнюдь не согласованного консенсуса в питона сообщества Некоторые люди предпочитают использовать. 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.

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

    • Mixin вызывает super().__init__()и передает любые аргументы, которые он получает.
    • Подкласс наследует от Mixin первого : class FooBar(FooMixin, Bar). Если порядок базовых классов неправильный, конструктор миксина никогда не будет вызван.
  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__(). Если бы они этого не сделали, кооперативного наследства не было бы.

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

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

Аран-Fey
источник
2
Твое второе замечание поразило меня. Я только когда-либо видел Mixins справа от настоящего суперкласса и думал, что они довольно свободные и опасные, так как вы не можете проверить, имеет ли класс, с которым вы смешиваетесь, те атрибуты, которые вы ожидаете от него. Я никогда не думал о том, чтобы поместить генерала super().__init__(*args, **kwargs)в миксин и написать его первым. Это имеет столько смысла.
Minix
10

Любой подход («новый стиль» или «старый стиль») будет работать, если у вас есть контроль над исходным кодом для 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

Класс 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сначала позвони, потом явный звонок. Обратите внимание, что в каждом случае со сторонними родителями требуется уникальный класс адаптера.

Таким образом, кажется, что если я не знаю / не контролирую init классов, которые я наследую ( 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")
Jundiaius
источник
1
Лучший ответ имхо. Нашел это особенно полезным, так как это более перспективно
Стивен Эллвуд
3

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

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

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

Вот более техническая статья с официального сайта Python:

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

кельвин
источник
2

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

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

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

Бен
источник
@RaymondHettinger Ну, вы уже написали и связали со статьей с некоторыми мыслями об этом в своем ответе, поэтому я не думаю, что мне есть что добавить к этому. :) Я не думаю, что возможно вообще адаптировать любой неиспользующий класс к супериерархии; Вы должны найти решение, адаптированное к конкретным классам.
Бен