it-swarm.com.ru

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

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

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

119
Adam Parkin

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

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

57
Raymond Hettinger

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

Есть 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- это моя личная рекомендация и ни в коем случае не согласованный консенсус в 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. Один из классов - миксин.

    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__(). Если бы они этого не сделали, кооперативного наследства не было бы.

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

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

27
Aran-Fey

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

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/

3
Kelvin

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

2
Nathaniel Jones

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

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

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

2
Ben

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