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

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

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

Обратите внимание, что init 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

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

Ответы [ 6 ]

57 голосов
/ 06 марта 2012

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

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

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

[Ответ на вопрос с последующим редактированием]

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

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

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

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

29 голосов
/ 22 мая 2018

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

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

    A 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__() и передает все полученные аргументы.
    • Подкласс наследует от mixin first : 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__(). Если бы они этого не сделали, кооперативного наследования не было бы.

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

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

3 голосов
/ 06 сентября 2012

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

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

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

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

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

2 голосов
/ 09 апреля 2019

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

2 голосов
/ 06 марта 2012

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

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

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

1 голос
/ 22 мая 2018

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