Nazywanie klasy nadrzędnej __init__ z dziedziczeniem wielokrotnym, jaki jest właściwy sposób?

174

Powiedzmy, że mam scenariusz wielokrotnego dziedziczenia:

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?

Są dwa typowe podejścia do pisania C„s __init__:

  1. (w starym stylu) ParentClass.__init__(self)
  2. (nowszy styl) super(DerivedClass, self).__init__()

Jednak w obu przypadkach, jeśli klasy nadrzędne ( Ai B) nie są zgodne z tą samą konwencją, kod nie będzie działał poprawnie (niektóre mogą zostać pominięte lub wywoływane wielokrotnie).

Więc jaki jest właściwy sposób? Łatwo jest powiedzieć „po prostu być spójne, wykonaj jedną lub drugą stronę”, ale jeśli Aalbo Bsą z 3rd biblioteki partii, co wtedy? Czy istnieje podejście, które zapewni wywołanie wszystkich konstruktorów klas nadrzędnych (i we właściwej kolejności i tylko raz)?

Edycja: aby zobaczyć, co mam na myśli, jeśli to zrobię:

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

Wtedy otrzymuję:

Entering C
Entering A
Entering B
Leaving B
Leaving A
Entering B
Leaving B
Leaving C

Zauważ, że Binit jest wywoływany dwukrotnie. Jeśli zrobię:

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

Wtedy otrzymuję:

Entering C
Entering A
Leaving A
Leaving C

Zauważ, że Binit nigdy nie jest wywoływany. Wygląda więc na to, że dopóki nie znam / nie kontroluję init klas, z których dziedziczę ( Ai B) nie mogę dokonać bezpiecznego wyboru dla klasy, którą piszę ( C).

Adam Parkin
źródło

Odpowiedzi:

78

Oba sposoby działają dobrze. Stosowane podejście super()prowadzi do większej elastyczności podklas.

W metodzie bezpośredniego połączenia C.__init__można wywołać zarówno A.__init__i B.__init__.

Podczas używania super()klasy muszą być zaprojektowane do wspólnego dziedziczenia wielokrotnego, gdzie Cwywołania super, które wywołują Akod, który będzie również wywoływał kod superwywołujący Bkod. Więcej informacji o tym, co można zrobić, można znaleźć pod adresem http://rhettinger.wordpress.com/2011/05/26/super-considered-supersuper .

[Pytanie odpowiedzi w późniejszej edycji]

Wygląda więc na to, że jeśli nie znam / nie kontroluję initów klas, które dziedziczę z (A i B), nie mogę dokonać bezpiecznego wyboru dla klasy, którą piszę (C).

W artykule, do którego istnieje odwołanie, pokazano, jak poradzić sobie z tą sytuacją, dodając klasę otoki wokół Ai B. Opracowany przykład znajduje się w części zatytułowanej „Jak włączyć klasę niechętną współpracy”.

Można by chcieć, aby wielokrotne dziedziczenie było łatwiejsze, pozwalając bez wysiłku komponować klasy samochodów i samolotów, aby uzyskać FlyingCar, ale rzeczywistość jest taka, że ​​oddzielnie zaprojektowane komponenty często wymagają adapterów lub opakowań, zanim zostaną połączone tak płynnie, jak byśmy chcieli :-)

Jeszcze jedna myśl: jeśli nie jesteś zadowolony z funkcji komponowania przy użyciu wielokrotnego dziedziczenia, możesz użyć kompozycji, aby mieć pełną kontrolę nad tym, które metody są wywoływane w jakich sytuacjach.

Raymond Hettinger
źródło
4
Nie, nie robią. Jeśli init B nie wywoła super, to init B nie zostanie wywołany, jeśli wykonamy to super().__init__()podejście. Jeśli sprawdzam A.__init__()i B.__init__()bezpośrednio (jeśli A i B dzwonią super) otrzymuję wielokrotne wywołanie init B.
Adam Parkin,
3
@AdamParkin (odnosząc się do twojego pytania jako edytowanego): Jeśli jedna z klas nadrzędnych nie jest przeznaczona do użytku z super () , zwykle można ją opakować w sposób, który dodaje super wywołanie. Przywołany artykuł przedstawia wypracowany przykład w sekcji zatytułowanej „Jak włączyć klasę niechętną współpracy”.
Raymond Hettinger
1
Jakoś udało mi się przegapić tę sekcję, gdy przeczytałem artykuł. Dokładnie to, czego szukałem. Dzięki!
Adam Parkin
1
Jeśli piszesz w Pythonie (miejmy nadzieję, że 3!) I korzystasz z dziedziczenia dowolnego rodzaju, ale szczególnie wielokrotnego, to czytanie rhettinger.wordpress.com/2011/05/26/super-considered-super powinno być wymagane.
Shawn Mehan
1
Głosowanie za głosem, ponieważ w końcu wiemy, dlaczego nie mamy latających samochodów, skoro byliśmy już pewni, że już tak będzie.
msouth
65

Odpowiedź na Twoje pytanie zależy od jednego bardzo ważnego aspektu: czy Twoje klasy bazowe są przeznaczone do wielokrotnego dziedziczenia?

Istnieją 3 różne scenariusze:

  1. Klasy bazowe są niepowiązanymi, samodzielnymi klasami.

    Jeśli swoją bazę zajęcia są odrębnymi podmiotami, które są w stanie funkcjonować niezależnie i nie znają się nawzajem, oni nie przeznaczone do wielokrotnego dziedziczenia. Przykład:

    class Foo:
        def __init__(self):
            self.foo = 'foo'
    
    class Bar:
        def __init__(self, bar):
            self.bar = bar

    Ważne: Zauważ, że ani Fooani nie Bardzwoni super().__init__()! Dlatego twój kod nie działał poprawnie. Ze względu na sposób, w jaki dziedziczenie diamentów działa w Pythonie, nie należy wywoływać klas, których klasa bazowa jestobjectsuper().__init__() . Jak zauważyłeś, spowodowałoby to przerwanie dziedziczenia wielokrotnego, ponieważ w końcu dzwonisz do innej klasy, __init__a nie object.__init__(). ( Uwaga: Unikanie super().__init__()w object-subclasses jest moja osobista rekomendacja i bynajmniej uzgodnione konsensusu w społeczności Pythona Niektórzy ludzie wolą używać. superW każdej klasie, twierdząc, że zawsze można napisać adapter jeśli klasa nie zachowuje się jak oczekujesz.)

    Oznacza to również, że nigdy nie powinieneś pisać klasy, która dziedziczy po object i nie ma __init__metody. Brak zdefiniowania __init__metody w ogóle ma taki sam efekt jak wywołanie super().__init__(). Jeśli twoja klasa dziedziczy bezpośrednio z object, upewnij się, że dodajesz pusty konstruktor, taki jak:

    class Base(object):
        def __init__(self):
            pass

    W każdym razie w tej sytuacji będziesz musiał ręcznie wywołać każdego konstruktora nadrzędnego. Można to zrobić na dwa sposoby:

    • Bez super

      class FooBar(Foo, Bar):
          def __init__(self, bar='bar'):
              Foo.__init__(self)  # explicit calls without super
              Bar.__init__(self, bar)
    • Z 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

    Każda z tych dwóch metod ma swoje zalety i wady. Jeśli używasz super, Twoja klasa będzie obsługiwać iniekcję zależności . Z drugiej strony łatwiej jest popełniać błędy. Na przykład, jeśli zmienisz kolejność Fooi Bar(lubisz class FooBar(Bar, Foo)), musisz zaktualizować superwywołania, aby były zgodne. Bez superCiebie nie musisz się tym martwić, a kod jest dużo bardziej czytelny.

  2. Jedną z klas jest mixin.

    Wstawek jest klasa, która jest przeznaczona do stosowania z wielokrotnego dziedziczenia. Oznacza to, że nie musimy ręcznie wywoływać obu konstruktorów nadrzędnych, ponieważ mixin automatycznie wywoła za nas drugi konstruktor. Ponieważ tym razem musimy wywołać tylko jeden konstruktor, możemy to zrobić, superaby uniknąć konieczności kodowania na stałe nazwy klasy nadrzędnej.

    Przykład:

    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.

    Oto ważne szczegóły:

    • Mixin wywołuje super().__init__()i przepuszcza wszystkie otrzymane argumenty.
    • W Podklasa dziedziczy z wstawek pierwszy : class FooBar(FooMixin, Bar). Jeśli kolejność klas bazowych jest nieprawidłowa, konstruktor miksera nigdy nie zostanie wywołany.
  3. Wszystkie klasy bazowe są przeznaczone do dziedziczenia kooperatywnego.

    Klasy przeznaczone do dziedziczenia kooperacyjnego są bardzo podobne do miksów: przechodzą przez wszystkie nieużywane argumenty do następnej klasy. Tak jak poprzednio, musimy tylko zadzwonićsuper().__init__() a wszystkie konstruktory nadrzędne będą wywoływane łańcuchowo.

    Przykład:

    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

    W tym przypadku kolejność klas nadrzędnych nie ma znaczenia. Równie dobrze moglibyśmy dziedziczyć po CoopBarpierwszym, a kod nadal działałby tak samo. Ale to prawda tylko dlatego, że wszystkie argumenty są przekazywane jako argumenty słów kluczowych. Użycie argumentów pozycyjnych ułatwiłoby uzyskanie nieprawidłowej kolejności argumentów, więc w zwyczaju klasy współpracujące przyjmują tylko argumenty słów kluczowych.

    To także wyjątek od reguły, o której wspomniałem wcześniej: Obie CoopFooi CoopBardziedziczą object, ale nadal dzwonią super().__init__(). Gdyby tego nie zrobili, nie byłoby spółdzielczego dziedziczenia.

Konkluzja: Prawidłowa implementacja zależy od klas, z których dziedziczysz.

Konstruktor jest częścią publicznego interfejsu klasy. Jeśli klasa jest zaprojektowana jako mieszana lub do dziedziczenia kooperacyjnego, należy to udokumentować. Jeśli dokumenty nie wspominają o niczym takim, można bezpiecznie założyć, że klasa nie została zaprojektowana do wspólnego dziedziczenia wielokrotnego.

Aran-Fey
źródło
2
Twój drugi punkt mnie powalił. Widziałem tylko Mixiny po prawej stronie rzeczywistej superklasy i pomyślałem, że są dość luźne i niebezpieczne, ponieważ nie możesz sprawdzić, czy klasa, do której się wmieszasz, ma atrybuty, których się spodziewasz. Nigdy nie myślałem o umieszczeniu generała super().__init__(*args, **kwargs)w miksie i napisaniu go najpierw. To ma taki sens.
Minix
10

Każde podejście („nowy styl” lub „stary styl”) zadziała, jeśli masz kontrolę nad kodem źródłowym AiB . W przeciwnym razie może być konieczne użycie klasy adaptera.

Dostępny kod źródłowy: prawidłowe użycie „nowego stylu”

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

Tutaj kolejność rozwiązywania metod (MRO) dyktuje, co następuje:

  • C(A, B)Anajpierw dyktuje B. MRO jest C -> A -> B -> object.
  • super(A, self).__init__()dalej wzdłuż łańcucha MRO zainicjowane w C.__init__celu B.__init__.
  • super(B, self).__init__()dalej wzdłuż łańcucha MRO zainicjowane w C.__init__celu object.__init__.

Można powiedzieć, że ten przypadek jest przeznaczony do dziedziczenia wielokrotnego .

Dostępny kod źródłowy: prawidłowe użycie „starego stylu”

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

Tutaj MRO nie ma znaczenia, ponieważ A.__init__i B.__init__są wywoływane wyraźnie. class C(B, A):działałby równie dobrze.

Chociaż ten przypadek nie jest „przeznaczony” do dziedziczenia wielokrotnego w nowym stylu, jak poprzednio, dziedziczenie wielokrotne jest nadal możliwe.


A co, jeśli Ai Bjesteś z biblioteki innej firmy - tj. Nie masz kontroli nad kodem źródłowym AiB ? Krótka odpowiedź: należy zaprojektować klasę adaptera, która implementuje niezbędne superwywołania, a następnie użyć pustej klasy do zdefiniowania MRO (zobacz artykuł Raymonda Hettingera nasuper - szczególnie w sekcji „Jak włączyć klasę niewspółpracującą”).

Zewnętrzni rodzice: Anie wdraża super; Brobi

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

Klasa Adapterimplementuje, superdzięki czemu Cmożna zdefiniować MRO, który wchodzi w grę, gdy super(Adapter, self).__init__()jest wykonywany.

A jeśli jest odwrotnie?

Zewnętrzni rodzice: Anarzędzia super; Bnie

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

Tutaj ten sam wzorzec, z wyjątkiem tego, że kolejność wykonywania jest przełączana Adapter.__init__; supernajpierw wywołanie, potem jawne wywołanie. Zwróć uwagę, że każdy przypadek z nadrzędnymi osobami trzecimi wymaga unikalnej klasy adaptera.

Wygląda więc na to, że dopóki nie znam / nie kontroluję init klas, z których dziedziczę ( Ai B) nie mogę dokonać bezpiecznego wyboru dla klasy, którą piszę ( C).

Chociaż możesz obsłużyć przypadki, w których nie kontrolujesz kodu źródłowego Ai nie Bużywasz klasy adaptera, prawdą jest, że musisz wiedzieć, jak implementuje się init klas nadrzędnych super(jeśli w ogóle), aby to zrobić.

Nathaniel Jones
źródło
4

Jak Raymond powiedział w swojej odpowiedzi, bezpośrednie wywołanie A.__init__i B.__init__działa dobrze, a twój kod byłby czytelny.

Jednak nie używa łącza dziedziczenia między Ctymi klasami. Wykorzystanie tego linku zapewnia większą spójność i sprawia, że ​​ewentualne refaktoryzacje są łatwiejsze i mniej podatne na błędy. Przykład, jak to zrobić:

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")
Jundiaius
źródło
1
Najlepsza odpowiedź imho. Okazało się to szczególnie pomocne, ponieważ jest bardziej przyszłościowe
Stephen Ellwood
3

Ten artykuł pomaga wyjaśnić wspólne dziedziczenie wielokrotne:

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

Wspomina o użytecznej metodzie, mro()która pokazuje kolejność rozwiązywania metod. W swojej 2nd przykład, gdy dzwonisz superw AThe superwezwanie kontynuuje w MRO. Oto Bdlaczego następna klasa w kolejnościB init jest wywoływany po raz pierwszy.

Oto bardziej techniczny artykuł z oficjalnej strony Pythona:

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

kelwin
źródło
2

Jeśli mnożysz klasy podklasy z bibliotek innych firm, to nie, nie ma ślepego podejścia do wywoływania __init__metod klasy bazowej (lub jakichkolwiek innych metod), które faktycznie działają niezależnie od tego, jak zostały zaprogramowane klasy bazowe.

supersprawia, że możliwe do klas przeznaczonych do zapisu wspólnie realizować metody jako część kompleksu wielu drzew dziedziczenia, które nie muszą być znane do autora klasowej. Ale nie ma sposobu, aby użyć go do prawidłowego dziedziczenia z dowolnych klas, które mogą, ale nie muszą, używaćsuper .

Zasadniczo to, czy klasa ma być klasyfikowana do podklasy przy użyciu superlub z bezpośrednimi wywołaniami do klasy bazowej, jest właściwością, która jest częścią „publicznego interfejsu” klasy i jako taka powinna być udokumentowana. Jeśli korzystasz z bibliotek innych firm w sposób, jakiego oczekiwał autor biblioteki, a biblioteka ma rozsądną dokumentację, zwykle powie ci, co musisz zrobić, aby podklasować określone rzeczy. Jeśli nie, będziesz musiał spojrzeć na kod źródłowy klas, które tworzysz podklasy, i zobaczyć, jaka jest ich konwencja wywoływania klas bazowych. Jeśli łączysz wiele klas z jednej lub więcej bibliotek innych firm w sposób, jakiego nie spodziewali się autorzy biblioteki , konsekwentne wywoływanie metod superklasowych może być niemożliwe superklasowych; jeśli klasa A jest częścią hierarchii używającej, supera klasa B jest częścią hierarchii, która nie używa super, to żadna opcja nie jest gwarantowana. Musisz wymyślić strategię, która zadziała w każdym konkretnym przypadku.

Ben
źródło
@RaymondHettinger Cóż, już napisałeś i zamieściłeś link do artykułu z kilkoma przemyśleniami na ten temat w swojej odpowiedzi, więc nie sądzę, że mam do tego wiele do dodania. :) Nie sądzę jednak, aby możliwe było ogólne zaadaptowanie dowolnej klasy nie używającej superużywania do super hierarchii; musisz znaleźć rozwiązanie dostosowane do konkretnych zajęć.
Ben