Вызов родительского класса 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 из сторонней библиотеки, что тогда? Есть ли подход, который может гарантировать, что все конструкторы родительского класса будут вызваны (и в правильном порядке, и только один раз)?

Edit: чтобы увидеть, что я имею в виду, если я делаю:

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 вызывается дважды. если я do:

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).

5 ответов


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

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

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

[ответ на вопрос как в дальнейшем редактировать]

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

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

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

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


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

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

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

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

    в этом случае вам придется вызывать каждый родительский конструктор вручную. Это проще без super.

    пример:

    class Foo:
        def __init__(self):
            self.foo = 'foo'
    
    class Bar:
        def __init__(self, bar):
            self.bar = bar
    
    class FooBar(Foo, Bar):
        def __init__(self, bar='bar'):
            Foo.__init__(self)  # explicit calls without super
            Bar.__init__(self, bar)
    
            # To get the same results with `super`, you'd have to do this:
            #   super().__init__()
            #   super(Foo, self).__init__(bar)
            # Which is obviously much less intuitive.
    

    важно: обратите внимание, что ни Foo, ни Bar звонки super().__init__()! Вот почему ваш код работал неправильно. Из-за того, как наследование алмазов работает в python:

    • классы, базовый класс которых object не следует называть super().__init__(). Как вы заметили, что это нарушит множественное наследование, потому что вы в конечном итоге вызываете , а не object.__init__().
    • это также означает, что вы должны никогда не пишите класс, который наследуется от object и нет __init__ метод. Не определяя __init__ метод Вообще имеет тот же эффект, что и вызов super().__init__(). Если ваш класс наследует непосредственно от object, обязательно добавьте пустой конструктор, например Итак:

.

    class Base(object):
        def __init__(self):
            pass
  1. один из классов является mixin.

    A миксин - это класс, который предназначен для использования с множественным наследованием. Это означает, что нам не нужно вызывать оба родительских конструктора вручную, потому что mixin автоматически вызовет 2-й конструктор для нас. Поскольку на этот раз нам нужно вызвать только один конструктор, мы можем сделать это с помощью super чтобы избежать нужно жестко закодировать имя родительского класса.

    пример:

    class FooMixin:
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            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 первый: class FooBar(FooMixin, Bar). Если порядок базовых классов неправильный, конструктор mixin никогда не будет вызываться.
  2. все базовые классы предназначены для кооперативное наследство.

    классы, предназначенные для совместного наследования, очень похожи на mixins: они передают все неиспользуемые аргументы следующему классу. Как и раньше, мы просто должны позвонить super().__init__() и все родительские конструкторы будут вызываться цепочкой.

    пример:

    class CoopFoo:
        def __init__(self, **kwargs):
            super().__init__(**kwargs)
            self.foo = 'foo'
    
    class CoopBar:
        def __init__(self, bar, **kwargs):
            super().__init__(**kwargs)
            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__(). Если бы они этого не сделали, то не было бы совместного наследства.

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

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


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

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

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

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

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


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

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

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


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