Birden fazla kalıtımla __init__ ebeveyn sınıfını çağırmak, doğru yol nedir?


175

Birden fazla miras senaryom olduğunu söyle:

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?

Yazma için iki tipik yaklaşım var C'ler __init__:

  1. (eski tarz) ParentClass.__init__(self)
  2. (Daha yeni tarzı) super(DerivedClass, self).__init__()

Bununla birlikte, her iki durumda da, üst sınıflar ( Ave B) aynı kuralı izlemezse, kod düzgün çalışmaz (bazıları gözden kaçabilir veya birden çok kez çağrılabilir).

Peki yine doğru yol nedir? "Sadece tutarlı olun, birini ya da diğerini takip edin" demek kolaydır, ancak 3. taraf bir kütüphaneden geliyorsa Aya Bda bir kütüphaneden geliyorsa ne olacak? Tüm üst sınıf kurucularının çağrılmasını (ve doğru sırada ve yalnızca bir kez) sağlayabilecek bir yaklaşım var mı?

Düzenleme: ne demek istediğimi görmek için:

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

Sonra anladım:

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

O Not Bbireyin init iki defa çağrılır. Eğer yaparsam:

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

Sonra anladım:

Entering C
Entering A
Leaving A
Leaving C

O Not Bler çağrılan asla init'. Öyle görünüyor ki miras aldığım sınıfların initini bilmiyor / kontrol etmiyorsam ( Ave B) yazdığım sınıf için güvenli bir seçim yapamıyorum ( C).


Yanıtlar:


78

Her iki yol da iyi çalışıyor. Kullanım yaklaşımı super(), alt sınıflar için daha fazla esneklik sağlar.

Doğrudan arama yaklaşımında C.__init__hem A.__init__ve hem de arayabilirsiniz B.__init__.

Kullanırken super(), sınıflar Cçağrıları kooperatif çoklu miras için tasarlanması gerekir super, hangi Akod çağıran hangi aynı zamanda superhangi Bkod çağıran kodu çağıracak . Nelerin yapılabileceği hakkında daha fazla bilgi için http://rhettinger.wordpress.com/2011/05/26/super-considered-super adresine bakın super.

[Daha sonra düzenlendiği şekliyle yanıt sorusu]

Öyle görünüyor ki, (A ve B) miras aldığım sınıfların initini bilmiyor / kontrol etmiyorsam, yazdığım sınıf için güvenli bir seçim yapamıyorum (C).

Başvurulan makalede Ave çevresine bir sarmalayıcı sınıf ekleyerek bu durumun nasıl ele alınacağını gösterir B. "İşbirlikçi Olmayan Sınıf Nasıl Katılır" başlıklı bölümde bir çalışma örneği var.

Bir FlyingCar almak için Araba ve Uçak sınıflarını zahmetsizce oluşturmanıza izin veren birden fazla kalıtımın daha kolay olmasını isteyebilirsiniz, ancak gerçek şu ki, ayrı ayrı tasarlanmış bileşenler genellikle istediğimiz kadar sorunsuz bir şekilde takmadan önce adaptörlere veya paketleyicilere ihtiyaç duyuyor :-)

Başka bir düşünce: birden fazla miras kullanarak işlevsellik oluşturmaktan memnun değilseniz, hangi durumlarda hangi yöntemlerin çağrıldığı üzerinde tam kontrol için kompozisyon kullanabilirsiniz.


4
Hayır. B'nin init'i süper çağırmazsa, super().__init__()yaklaşımı yaparsak B'nin init'i çağrılmaz . Ben ararsam A.__init__()ve B.__init__()(A ve B çağrı yaparsanız doğrudan, daha sonra super) Ben B'nin init birden çok kez çağrıldığını olsun.
Adam Parkin

3
@AdamParkin (sorunuzla ilgili olarak düzenlenmiş olarak): Üst sınıflardan biri super () ile kullanılmak üzere tasarlanmamışsa , genellikle süper çağrıyı ekleyecek şekilde sarılabilir . Atıfta bulunulan makalede "İşbirlikçi Olmayan Sınıf Nasıl Katılır" başlıklı bölümde bir çalışma örneği gösterilmektedir.
Raymond Hettinger

1
Bir şekilde makaleyi okuduğumda bu bölümü kaçırmayı başardım. Tam aradığım şey. Teşekkürler!
Adam Parkin

1
Python (umarım 3!) Yazıyor ve herhangi bir tür miras kullanıyor, ancak özellikle çoklu, o zaman rhettinger.wordpress.com/2011/05/26/super-considered-super okunması gerekiyor.
Shawn Mehan

1
Upvoting çünkü sonunda sahip olacağımızdan emin olduğumuzda neden uçan arabalara sahip olmadığımızı biliyoruz.
15'te msouth

66

Sorunuzun cevabı çok önemli bir hususa bağlıdır: Temel sınıflarınız çoklu miras için mi tasarlandı?

3 farklı senaryo vardır:

  1. Temel sınıflar ilişkisiz, bağımsız sınıflardır.

    Baz sınıflar bağımsız işleyen yeteneğine sahip ayrı varlıklardır ve birbirlerini bilmiyorsanız, onlar konum değil birden miras için tasarlanmış. Misal:

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

    Önemli: Ne fark etmez ne Foode Barçağırır super().__init__()! Bu nedenle kodunuz düzgün çalışmadı. Çünkü Python yolu elmas miras eserlerinin, kimin temel sınıftır sınıflar objectçağırmamalıdırsuper().__init__() . Fark ettiğiniz gibi, bunu yapmak birden fazla mirasa neden olur çünkü sonunda başka bir sınıfı __init__çağırırsınız object.__init__(). ( Yasal Uyarı: kaçınmak super().__init__()içinde object-subclasses benim kişisel öneri ve hiçbir şekilde bir kararlaştırılmış piton toplumda konsensüs Bazı insanlar kullanıma tercih ederim. superHer zaman bir yazabilirsiniz savunarak, her sınıfta adaptörü sınıf olarak davranmaz ise beklediğiniz.)

    Bu aynı zamanda, miras alan objectve bir __init__yöntemi olmayan bir sınıfı asla yazmamanız gerektiği anlamına gelir . Bir __init__yöntem tanımlamamayı çağırmakla aynı etkiye sahiptir super().__init__(). Sınıfınız doğrudan kaynağından miras alırsa, aşağıdaki objectgibi boş bir kurucu eklediğinizden emin olun:

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

    Her neyse, bu durumda, her bir üst yapıcıyı manuel olarak çağırmanız gerekecektir. Bunu yapmanın iki yolu vardır:

    • olmadan super

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

    Bu iki yöntemin her birinin kendi avantajları ve dezavantajları vardır. Kullanırsanız super, sınıfınız bağımlılık enjeksiyonunu destekleyecektir . Öte yandan, hata yapmak daha kolaydır. Örneğin sırasını değiştirirseniz Foove Bar(gibi class FooBar(Bar, Foo)), güncellemek zorunda kalacak supermaçın çağrıları. Bu olmadan superendişelenmenize gerek yok ve kod çok daha okunabilir.

  2. Sınıflardan biri bir mixin.

    Bir mixin oluyor bir sınıftır tasarlanmış çoklu miras ile kullanılacak. Bu, her iki üst yapıcıyı manuel olarak çağırmamız gerekmediği anlamına gelir, çünkü mixin otomatik olarak 2. yapıcıyı bizim için arayacaktır. Bu sefer sadece tek bir kurucu çağırmamız gerektiğinden super, üst sınıfın adını sabit kodlamak zorunda kalmamak için bunu yapabiliriz .

    Misal:

    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.

    Burada önemli ayrıntılar:

    • Mixin super().__init__()alır ve aldığı argümanlardan geçer.
    • Mixin gelen alt sınıf devralır ilk : class FooBar(FooMixin, Bar). Temel sınıfların sırası yanlışsa, mixin'in yapıcısı asla çağrılmaz.
  3. Tüm temel sınıflar kooperatif mirası için tasarlanmıştır.

    İşbirlikçi miras için tasarlanan sınıflar, karışımlara çok benzer: Kullanılmayan tüm argümanları bir sonraki sınıfa geçirirler. Daha önce olduğu gibi, sadece aramak zorundayız super().__init__()ve tüm ana inşaatçılar zincir olarak adlandırılacak.

    Misal:

    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

    Bu durumda, üst sınıfların sırası önemli değildir. İlkinden de miras alabiliriz CoopBarve kod yine de aynı şekilde çalışır. Ama bu sadece doğru çünkü tüm argümanlar anahtar kelime argümanı olarak geçiyor. Konumsal bağımsız değişkenler kullanmak, bağımsız değişkenlerin sırasını yanlış almayı kolaylaştıracaktır; bu nedenle, kooperatif sınıflarının yalnızca anahtar kelime bağımsız değişkenlerini kabul etmesi gelenekseldir.

    Bu aynı zamanda daha önce bahsedilen kuralın bir istisnası: Her iki CoopFoove CoopBargelen inherit object, ama yine de diyoruz super().__init__(). Eğer olmasaydı, işbirlikçi bir miras olmazdı.

Alt satır: Doğru uygulama, devraldığınız sınıflara bağlıdır.

Yapıcı, sınıfın genel arabiriminin bir parçasıdır. Sınıf bir mixin veya kooperatif mirası olarak tasarlanmışsa, bu belgelenmelidir. Docs tür bir şey söz yoksa, o sınıf varsaymak güvenlidir değildir kooperatif çoklu miras için tasarlanmış.


2
İkinci noktan beni uçurdu. Sadece gerçek süper sınıfın sağında Mixins'i gördüm ve oldukça gevşek ve tehlikeli olduklarını düşündüm, çünkü karıştırdığınız sınıfın sahip olmasını beklediğiniz özelliklere sahip olup olmadığını kontrol edemezsiniz. Bir generali super().__init__(*args, **kwargs)miksere koyup önce yazmayı hiç düşünmemiştim . Çok mantıklı geliyor.
Minix

10

Her iki yaklaşım ( "yeni stil" veya "eski tarz") çalışacak için kaynak kodu üzerinde kontrol sahibi olmadığını AveB . Aksi takdirde, bir adaptör sınıfının kullanılması gerekebilir.

Kaynak kod erişilebilir: "Yeni stil" in doğru kullanımı

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

Burada, yöntem çözümleme sırası (MRO) aşağıdakileri belirler:

  • C(A, B)Aönce dikte eder , sonra B. MRO öyle C -> A -> B -> object.
  • super(A, self).__init__()için başlatılan MRO zinciri boyunca devam C.__init__eder B.__init__.
  • super(B, self).__init__()için başlatılan MRO zinciri boyunca devam C.__init__eder object.__init__.

Bu davanın birden fazla miras için tasarlandığını söyleyebilirsiniz .

Kaynak kod erişilebilir: "Eski stil" in doğru kullanımı

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

Çünkü Burada, MRO, fark etmez A.__init__ve B.__init__açıkça denir. class C(B, A):aynen işe yarar.

Her ne kadar bu durum yeni tarzda bir önceki gibi çoklu kalıtım için "tasarlanmamış" olsa da, çoklu kalıtım hala mümkündür.


Şimdi, eğer Ave Büçüncü taraf kitaplığından vardır - yani sizin için kaynak kodu üzerinde hiçbir kontrole sahip AveB ? Kısa cevap: Gerekli superçağrıları gerçekleştiren bir adaptör sınıfı tasarlamanız ve ardından MRO'yu tanımlamak için boş bir sınıf kullanmanız gerekir ( Raymond Hettinger'in şu makalesine bakınsuper - özellikle "İşbirlikçi Olmayan Sınıf Nasıl Katılır " bölümü).

Üçüncü taraf ebeveynler: Auygulamaz super; Byapar

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

Sınıf Adapteruygular super, böylece Cdevreye girer MRO, tanımlayabilirsiniz super(Adapter, self).__init__()yürütülür.

Ya başka türlü olursa?

Üçüncü taraf ebeveynler: Auygular super; Bdeğil

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

Burada aynı düzen, yürütme sırası açık değil Adapter.__init__; superönce ara, sonra açık ara. Üçüncü taraf ebeveynleri olan her bir vakanın benzersiz bir adaptör sınıfı gerektirdiğine dikkat edin.

Öyle görünüyor ki miras aldığım sınıfların initini bilmiyor / kontrol etmiyorsam ( Ave B) yazdığım sınıf için güvenli bir seçim yapamıyorum ( C).

Eğer yok davalarını rağmen kontrol kaynak kodunu Ave Bbir adaptör sınıfını kullanarak, size gereken doğrudur biliyoruz init üst sınıfların uygulamak var nasıl super(eğer hiç) bunu yapmak için de.


4

Raymond'un cevabında söylediği gibi, doğrudan bir çağrı A.__init__ve iyi B.__init__çalışır ve kodunuz okunabilir.

Ancak, Cbu sınıflar ile kalıtım bağlantısını kullanmaz . Bu bağlantıyı kullanmak size daha fazla tutarlılık sağlar ve nihai yeniden düzenlemeleri daha kolay ve daha az hataya açık hale getirir. Bunun nasıl yapılacağına dair bir örnek:

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
En iyi cevap imho. Daha geleceğe yönelik olduğu için bu özellikle yararlı bulundu
Stephen Ellwood

3

Bu makale, ortak çoklu mirasın açıklanmasına yardımcı olur:

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

mro()Size yöntem çözüm sırasını gösteren kullanışlı yöntemden bahseder . Aradığınızda senin 2 örnekte, superiçinde A, superçağrı MRO içinde devam eder. Sıradaki bir sonraki sınıf B, bu yüzden Binit'in ilk kez çağrılmasıdır.

İşte resmi python sitesinden daha teknik bir makale:

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


2

Alt sınıf sınıflarını üçüncü taraf kitaplıklarından çarpıyorsanız, hayır, __init__temel sınıfların nasıl programlandığından bağımsız olarak çalışan temel sınıf yöntemlerini (veya başka yöntemleri) çağırmak için kör bir yaklaşım yoktur .

supersınıf yazarı tarafından bilinmesi gerekmeyen karmaşık çoklu kalıtım ağaçlarının bir parçası olarak yöntemleri birlikte uygulamak üzere tasarlanmış sınıflar yazmayı mümkün kılar . Ancak, kullanabilecek veya kullanamayacak keyfi sınıflardan doğru bir şekilde miras almak için bunu kullanmanın bir yolu yoktur super.

Esasen, bir sınıfın, supertemel sınıfa doğrudan çağrılar kullanılarak veya doğrudan çağrılarak alt sınıf olarak tasarlanıp tasarlanmayacağı , sınıfın "" genel arabirimi "nin bir parçası olan bir özelliktir ve bu şekilde belgelenmelidir. Üçüncü taraf kitaplıkları, kitaplık yazarının beklediği şekilde ve kitaplığın makul belgelere sahip şekilde kullanıyorsanız, normalde size belirli şeyleri alt sınıflamak için ne yapmanız gerektiğini söyler. Değilse, alt sınıflandırma yaptığınız sınıflar için kaynak koduna bakmanız ve temel sınıf çağırma kurallarının ne olduğunu görmeniz gerekir. Eğer kütüphane yazarlar bir şekilde bir veya daha fazla üçüncü taraf kütüphanelerinden birden sınıfları birleştirerek ediyorsanız vermedi bekliyoruz, o zaman sürekli çağırmak süper sınıf yöntemlerine mümkün olmayabilir hiç; A sınıfı bir hiyerarşinin parçasıysa superve B sınıfı süper kullanmayan bir hiyerarşinin parçasıysa, her iki seçeneğin de çalışması garanti edilmez. Her bir vaka için işe yarayacak bir strateji bulmanız gerekecek.


@RaymondHettinger Cevabınızla ilgili bazı düşüncelere sahip bir makaleyi zaten yazdınız ve bağladınız, bu yüzden ekleyecek çok şeyim olduğunu düşünmüyorum. :) Süper kullanmayan herhangi bir sınıfı bir süper hiyerarşiye genel olarak uyarlamanın mümkün olduğunu düşünmüyorum; ilgili sınıflara uyarlanmış bir çözüm bulmalısınız.
Ben
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.