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

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

5 89

5 ответов:

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

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

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

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

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

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

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

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

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

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

.

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

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

    классы, предназначенные для совместного наследования, очень похожи на миксины: они проходят через все неиспользуемые аргументы в следующий класс. Как и раньше, мы просто должны позвонить 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 in A на super вызов продолжается в MRO. Следующий класс в порядке B, поэтому B ' s init называется в первый раз.

вот более технический статья с официального сайта 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")