Виклик батьківського класу __init__ з багаторазовим успадкуванням, який правильний спосіб?


174

Скажіть, у мене є декілька сценаріїв успадкування:

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є з 3 - сторонньої бібліотеки, що тоді? Чи існує підхід, який може забезпечити виклик усіх конструкторів батьківського класу (у правильному порядку та лише один раз)?

Редагувати: щоб побачити, що я маю на увазі, якщо це зробити:

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


Відповіді:


78

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

У підході до прямого дзвінка C.__init__можуть дзвонити і A.__init__і B.__init__.

При використанні super(), класи повинні бути розроблені для спільного множинного спадкоємства , де Cвиклики super, які викликають A«s коду , який також називають super, активізує B» код с. Докладніші відомості про те, що можна зробити, див. На веб-сайті http://rhettinger.wordpress.com/2011/05/26/super-considered-supersuper .

[Питання відповіді, як згодом відредаговано]

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

Посилана стаття показує, як вирішити цю ситуацію, додавши клас обгортки навколо AтаB . Є розроблений приклад у розділі під назвою "Як включити клас, що не є кооперативом".

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

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


4
Ні, вони ні. Якщо init B не викликає супер, то init B не буде викликаний, якщо ми виконаємо super().__init__()підхід. Якщо я дзвоню A.__init__()і B.__init__()безпосередньо, тоді (якщо A і B дійсно дзвонять super), я отримую B init, який викликається кілька разів.
Адам Паркін

3
@AdamParkin (стосовно Вашого запитання в редагуванні): Якщо один з батьківських класів не призначений для використання з Super () , він, як правило, може бути перетворений так, що додає супервиклик . Згадана стаття демонструє відпрацьований приклад у розділі "Як включити клас, що не є кооперативом".
Реймонд Хеттінгер

1
Якось мені вдалося пропустити цей розділ, коли я прочитав статтю. Саме те, що я шукав. Дякую!
Адам Паркін

1
Якщо ви пишете python (сподіваємось, 3!) І використовуєте спадщину будь-якого типу, але особливо багаторазового, тоді слід прочитати rhettinger.wordpress.com/2011/05/26/super-considered-super .
Шон Механ

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

65

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

Існує 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. Один із занять - міксин.

    Mixin клас , який призначений для використання множинного спадкоємства. Це означає, що нам не доведеться викликати обох батьківських конструкторів вручну, тому що 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 першого : class FooBar(FooMixin, Bar). Якщо порядок базових класів невірний, конструктор mixin ніколи не буде викликаний.
  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__(). Якби не вони, кооперативної спадщини не було б.

Підсумок: Правильна реалізація залежить від класів, які ви успадковуєте.

Конструктор є частиною публічного інтерфейсу класу. Якщо клас розроблений як міксин або для кооперативного спадкування, це повинно бути задокументовано. Якщо документи не згадують нічого подібного, можна припустити, що клас не призначений для кооперативного множинного успадкування.


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

Тож здається, що якщо я не знаю / не контролюю ініціативи класів, які я успадковую від ( 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")

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 є частиною ієрархії, яка не використовує супер, тоді жоден варіант не гарантовано працює. Вам доведеться розробити стратегію, яка працює в кожному конкретному випадку.


@RaymondHettinger Ну, ви вже писали та пов'язували статтю з деякими думками з цього приводу у своїй відповіді, тому я не думаю, що мені до цього багато чого додати. :) Я не думаю, що можливо загально пристосувати будь-який клас, який не використовує супер, до суперієрархії; Ви повинні придумати рішення, пристосоване до конкретних занять.
Бен
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.