Memanggil kelas induk __init__ dengan multiple inheritance, apa cara yang benar?

174

Katakanlah saya memiliki beberapa skenario warisan:

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?

Ada dua pendekatan yang khas untuk menulis C's __init__:

  1. (gaya lama) ParentClass.__init__(self)
  2. (gaya baru) super(DerivedClass, self).__init__()

Namun, dalam kedua kasus, jika kelas induk ( Adan B) tidak mengikuti konvensi yang sama, maka kode tidak akan berfungsi dengan benar (beberapa mungkin terlewatkan, atau dipanggil beberapa kali).

Jadi apa cara yang benar lagi? Sangat mudah untuk mengatakan "konsisten saja, ikuti satu atau yang lain", tetapi jika Aatau Bberasal dari perpustakaan pihak ke-3, lalu bagaimana? Apakah ada pendekatan yang dapat memastikan bahwa semua konstruktor kelas induk dipanggil (dan dalam urutan yang benar, dan hanya sekali)?

Edit: untuk melihat apa yang saya maksud, jika saya lakukan:

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

Lalu saya mendapatkan:

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

Perhatikan bahwa Binit dipanggil dua kali. Jika aku melakukan:

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

Lalu saya mendapatkan:

Entering C
Entering A
Leaving A
Leaving C

Perhatikan bahwa Binit tidak pernah dipanggil. Jadi sepertinya kecuali saya tahu / mengontrol init tentang kelas yang saya warisi dari ( Adan B) saya tidak bisa membuat pilihan yang aman untuk kelas yang saya tulis ( C).

Adam Parkin
sumber

Jawaban:

78

Keduanya bekerja dengan baik. Pendekatan menggunakan super()mengarah pada fleksibilitas yang lebih besar untuk subkelas.

Dalam pendekatan panggilan langsung, C.__init__dapat memanggil keduanya A.__init__dan B.__init__.

Saat menggunakan super(), kelas-kelas perlu dirancang untuk multiple inheritance kooperatif di mana Cpanggilan super, yang memanggil Akode yang juga akan memanggil superyang memanggil Bkode itu. Lihat http://rhettinger.wordpress.com/2011/05/26/super-considered-super untuk detail lebih lanjut tentang apa yang dapat dilakukan super.

[Pertanyaan tanggapan yang kemudian diedit]

Jadi sepertinya kecuali saya tahu / mengendalikan init tentang kelas yang saya warisi dari (A dan B) saya tidak bisa membuat pilihan yang aman untuk kelas yang saya tulis (C).

Artikel yang direferensikan menunjukkan bagaimana menangani situasi ini dengan menambahkan kelas pembungkus di sekitar Adan B. Ada contoh yang berhasil di bagian berjudul "Cara Memasukkan Kelas Non-kooperatif".

Orang mungkin berharap bahwa pewarisan berganda lebih mudah, memungkinkan Anda dengan mudah membuat kelas Mobil dan Pesawat untuk mendapatkan FlyingCar, tetapi kenyataannya komponen yang dirancang secara terpisah sering membutuhkan adaptor atau pembungkus sebelum dipasang bersama dengan mulus seperti yang kita inginkan :-)

Satu pemikiran lain: jika Anda tidak senang menyusun fungsi menggunakan banyak pewarisan, Anda dapat menggunakan komposisi untuk kontrol penuh atas metode mana yang dipanggil pada kesempatan mana.

Raymond Hettinger
sumber
4
Tidak, mereka tidak. Jika init B tidak memanggil super, maka init B tidak akan dipanggil jika kita melakukan super().__init__()pendekatan. Jika saya menelepon A.__init__()dan B.__init__()langsung, maka (jika A dan B melakukan panggilan super) saya mendapatkan init B dipanggil beberapa kali.
Adam Parkin
3
@AdamParkin (mengenai pertanyaan Anda yang telah diedit): Jika salah satu kelas induk tidak dirancang untuk digunakan dengan super () , biasanya dapat dibungkus dengan cara yang menambahkan panggilan super . Artikel yang direferensikan menunjukkan contoh yang berhasil di bagian berjudul "Cara Memasukkan Kelas Non-kooperatif".
Raymond Hettinger
1
Entah bagaimana saya berhasil melewatkan bagian itu ketika saya membaca artikel itu. Persis apa yang saya cari. Terima kasih!
Adam Parkin
1
Jika Anda menulis python (mudah-mudahan 3!) Dan menggunakan warisan apa pun, tetapi terutama berganda, maka rhettinger.wordpress.com/2011/05/26/super-considered-super harus dibaca.
Shawn Mehan
1
Terburuk karena akhirnya kami tahu mengapa kami tidak punya mobil terbang ketika kami yakin akan memilikinya sekarang.
msouth
65

Jawaban atas pertanyaan Anda tergantung pada satu aspek yang sangat penting: Apakah kelas dasar Anda dirancang untuk pewarisan berganda?

Ada 3 skenario berbeda:

  1. Kelas dasar adalah kelas mandiri, yang tidak terkait.

    Jika kelas dasar Anda adalah entitas terpisah yang mampu berfungsi secara independen dan mereka tidak mengenal satu sama lain, mereka tidak dirancang untuk pewarisan berganda. Contoh:

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

    Penting: Perhatikan bahwa tidak satu Foopun Barpanggilan super().__init__()! Inilah sebabnya mengapa kode Anda tidak berfungsi dengan benar. Karena cara kerja pewarisan intan dalam python, kelas yang kelas dasarnya objecttidak boleh dipanggilsuper().__init__() . Seperti yang Anda perhatikan, hal itu akan merusak banyak warisan karena Anda akhirnya memanggil kelas lain __init__daripada object.__init__(). ( Disclaimer: Menghindari super().__init__()di object-subclasses adalah rekomendasi pribadi saya dan tidak berarti yang telah disepakati konsensus dalam komunitas python Sebagian orang memilih untuk digunakan. superDi setiap kelas, dengan alasan bahwa Anda selalu dapat menulis adapter jika kelas tidak berperilaku seperti Anda harapkan.)

    Ini juga berarti bahwa Anda tidak boleh menulis kelas yang mewarisi objectdan tidak memiliki __init__metode. Tidak mendefinisikan __init__metode sama sekali memiliki efek yang sama dengan menelepon super().__init__(). Jika kelas Anda mewarisi langsung dari object, pastikan untuk menambahkan konstruktor kosong seperti:

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

    Bagaimanapun, dalam situasi ini, Anda harus memanggil setiap induk konstruktor secara manual. Ada dua cara untuk melakukan ini:

    • Tanpa super

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

    Masing-masing dari dua metode ini memiliki kelebihan dan kekurangan. Jika Anda menggunakan super, kelas Anda akan mendukung injeksi ketergantungan . Di sisi lain, lebih mudah melakukan kesalahan. Misalnya jika Anda mengubah urutan Foodan Bar(suka class FooBar(Bar, Foo)), Anda harus memperbarui superpanggilan yang cocok. Tanpa superAnda tidak perlu khawatir tentang ini, dan kode jauh lebih mudah dibaca.

  2. Salah satu kelas adalah mixin.

    Sebuah mixin adalah kelas yang dirancang untuk digunakan dengan multiple inheritance. Ini berarti kita tidak perlu memanggil kedua konstruktor induk secara manual, karena mixin akan secara otomatis memanggil konstruktor kedua untuk kita. Karena kita hanya perlu memanggil konstruktor tunggal kali ini, kita dapat melakukannya dengan supermenghindari keharusan membuat kode nama kelas induk.

    Contoh:

    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.

    Detail penting di sini adalah:

    • Mixin memanggil super().__init__()dan melewati argumen apa pun yang diterimanya.
    • Subclass mewarisi dari mixin pertama : class FooBar(FooMixin, Bar). Jika urutan kelas dasar salah, konstruktor mixin tidak akan pernah dipanggil.
  3. Semua kelas dasar dirancang untuk pewarisan kooperatif.

    Kelas yang dirancang untuk pewarisan kooperatif sangat mirip dengan mixin: Mereka melewati semua argumen yang tidak digunakan ke kelas berikutnya. Seperti sebelumnya, kita hanya perlu meneleponsuper().__init__() dan semua konstruktor induk akan dipanggil berantai.

    Contoh:

    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

    Dalam hal ini, urutan kelas induk tidak menjadi masalah. Kita mungkin juga mewarisi dari CoopBardulu, dan kodenya akan tetap sama. Tapi itu hanya benar karena semua argumen dilewatkan sebagai argumen kata kunci. Menggunakan argumen posisional akan membuatnya mudah untuk mendapatkan urutan argumen yang salah, sehingga biasanya kelas koperasi hanya menerima argumen kata kunci.

    Ini juga merupakan pengecualian terhadap aturan yang saya sebutkan sebelumnya: Keduanya CoopFoodan CoopBarmewarisi dari object, tetapi mereka masih memanggil super().__init__(). Jika tidak, tidak akan ada warisan kooperatif.

Intinya: Implementasi yang benar tergantung pada kelas yang Anda warisi.

Konstruktor adalah bagian dari antarmuka publik kelas. Jika kelas dirancang sebagai mixin atau untuk warisan koperasi, itu harus didokumentasikan. Jika dokumen tidak menyebutkan hal semacam itu, aman untuk mengasumsikan bahwa kelas tidak dirancang untuk pewarisan berganda.

Aran-Fey
sumber
2
Poin kedua Anda mengejutkan saya. Saya hanya pernah melihat Mixins di sebelah kanan kelas super yang sebenarnya dan berpikir mereka cukup longgar dan berbahaya, karena Anda tidak dapat memeriksa apakah kelas yang Anda gabungkan memiliki atribut yang Anda harapkan dimiliki. Saya tidak pernah berpikir untuk memasukkan seorang jenderal super().__init__(*args, **kwargs)ke dalam mixin dan menulisnya terlebih dahulu. Sangat masuk akal.
Minix
10

Baik pendekatan ("gaya baru" atau "gaya lama") akan berfungsi jika Anda memiliki kendali atas kode sumber untuk AdanB . Jika tidak, penggunaan kelas adaptor mungkin diperlukan.

Kode sumber dapat diakses: Penggunaan "gaya baru" dengan benar

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

Di sini, metode resolusi pesanan (MRO) menentukan berikut ini:

  • C(A, B)menentukan Aterlebih dahulu, lalu B. MRO adalahC -> A -> B -> object .
  • super(A, self).__init__()terus sepanjang rantai MRO dimulai pada C.__init__ke B.__init__.
  • super(B, self).__init__()terus sepanjang rantai MRO dimulai pada C.__init__ke object.__init__.

Anda bisa mengatakan bahwa case ini dirancang untuk multiple inheritance .

Kode sumber dapat diakses: Penggunaan "gaya lama" dengan benar

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

Di sini, MRO tidak penting, karena A.__init__dan B.__init__disebut secara eksplisit. class C(B, A):akan bekerja dengan baik.

Meskipun case ini tidak "dirancang" untuk multiple inheritance dengan style baru seperti sebelumnya, multiple inheritance masih dimungkinkan.


Sekarang, bagaimana jika Adan Bberasal dari perpustakaan pihak ketiga - yaitu, Anda tidak memiliki kendali atas kode sumber untuk AdanB ? Jawaban singkat: Anda harus merancang kelas adaptor yang mengimplementasikan superpanggilan yang diperlukan , kemudian menggunakan kelas kosong untuk mendefinisikan MRO (lihat artikel Raymond Hettinger padasuper - terutama bagian, "Cara Memasukkan Kelas Non-kooperatif").

Orang tua pihak ketiga: Atidak menerapkan super; Btidak

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

Kelas Adaptermengimplementasikan supersehingga Cdapat mendefinisikan MRO, yang ikut bermain ketika super(Adapter, self).__init__()dieksekusi.

Dan bagaimana jika sebaliknya?

Orang tua pihak ketiga: Aimplementasi super; Btidak

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

Pola yang sama di sini, kecuali urutan eksekusi diaktifkan Adapter.__init__; superpanggilan dulu, lalu panggilan eksplisit. Perhatikan bahwa setiap case dengan orang tua pihak ketiga membutuhkan kelas adaptor yang unik.

Jadi sepertinya kecuali saya tahu / mengontrol init tentang kelas yang saya warisi dari ( Adan B) saya tidak bisa membuat pilihan yang aman untuk kelas yang saya tulis ( C).

Meskipun Anda dapat menangani kasus di mana Anda tidak mengontrol kode sumber Adan Bdengan menggunakan kelas adaptor, memang benar bahwa Anda harus tahu bagaimana init dari kelas induk menerapkan super(jika sama sekali) untuk melakukannya.

Nathaniel Jones
sumber
4

Seperti yang dikatakan Raymond dalam jawabannya, panggilan langsung ke A.__init__dan B.__init__berfungsi dengan baik, dan kode Anda akan terbaca.

Namun, itu tidak menggunakan tautan warisan antara Cdan kelas-kelas itu. Memanfaatkan tautan itu memberi Anda lebih banyak konsistensi dan membuat refactoring akhirnya lebih mudah dan lebih sedikit rawan kesalahan. Contoh cara melakukannya:

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
sumber
1
Jawaban terbaik imho. Menemukan ini sangat membantu karena lebih futureproof
Stephen Ellwood
3

Artikel ini membantu menjelaskan beberapa pewarisan kooperatif:

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

Ini menyebutkan metode mro()yang berguna yang menunjukkan kepada Anda urutan resolusi metode. Dalam contoh 2 Anda, di mana Anda menelepon superdi A, yang superpanggilan terus di dalam MRO. Kelas berikutnya dalam urutannya adalah B, inilah mengapa Binit disebut pertama kali.

Inilah artikel yang lebih teknis dari situs python resmi:

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

Kelvin
sumber
2

Jika Anda mengalikan kelas sub-kelas dari perpustakaan pihak ketiga, maka tidak, tidak ada pendekatan buta untuk memanggil kelas dasar __init__ metode (atau metode lain) yang benar-benar berfungsi terlepas dari bagaimana kelas dasar diprogram.

supermembuatnya mungkin untuk kelas menulis dirancang untuk kooperatif menerapkan metode sebagai bagian dari pohon warisan beberapa kompleks yang tidak perlu diketahui penulis kelas. Tetapi tidak ada cara untuk menggunakannya dengan benar mewarisi dari kelas sewenang-wenang yang mungkin atau mungkin tidak digunakan super.

Pada dasarnya, apakah kelas dirancang untuk sub-kelas menggunakan superatau dengan panggilan langsung ke kelas dasar adalah properti yang merupakan bagian dari "antarmuka publik" kelas, dan harus didokumentasikan seperti itu. Jika Anda menggunakan perpustakaan pihak ketiga dengan cara yang diharapkan oleh pembuat perpustakaan dan perpustakaan memiliki dokumentasi yang masuk akal, biasanya akan memberi tahu Anda apa yang harus Anda lakukan untuk mensubklasifikasikan hal-hal tertentu. Jika tidak, maka Anda harus melihat kode sumber untuk kelas yang Anda sub-klasifikasi dan melihat apa konvensi pemanggilan kelas-dasar mereka. Jika Anda menggabungkan beberapa kelas dari satu atau lebih perpustakaan pihak ketiga dengan cara yang tidak diharapkan oleh penulis perpustakaan , maka mungkin tidak mungkin untuk secara konsisten memanggil metode kelas super sama sekali; jika kelas A adalah bagian dari hierarki menggunakansuperdan kelas B adalah bagian dari hierarki yang tidak menggunakan super, maka tidak ada opsi yang dijamin untuk bekerja. Anda harus memikirkan strategi yang sesuai untuk setiap kasus tertentu.

Ben
sumber
@RaymondHettinger Yah, Anda sudah menulis dan menautkan ke artikel dengan beberapa pemikiran tentang itu dalam jawaban Anda, jadi saya rasa saya tidak punya banyak hal untuk ditambahkan pada itu. :) Saya tidak berpikir itu mungkin untuk secara umum mengadaptasi kelas non-super-gunakan ke super-hierarki; Anda harus datang dengan solusi yang disesuaikan dengan kelas-kelas tertentu yang terlibat.
Ben