Gọi lớp cha mẹ __init__ với nhiều kế thừa, cách nào đúng?


174

Nói rằng tôi có một kịch bản thừa kế:

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ó hai cách tiếp cận điển hình để viết C's __init__:

  1. (phong cách cũ) ParentClass.__init__(self)
  2. (kiểu mới hơn) super(DerivedClass, self).__init__()

Tuy nhiên, trong cả hai trường hợp, nếu các lớp cha ( AB) không tuân theo cùng một quy ước, thì mã sẽ không hoạt động chính xác (một số có thể bị bỏ qua hoặc được gọi nhiều lần).

Vì vậy, những gì đúng cách một lần nữa? Thật dễ dàng để nói "chỉ cần nhất quán, theo dõi cái này hay cái khác", nhưng nếu Ahoặc Blà từ thư viện của bên thứ 3 thì sao? Có một cách tiếp cận nào có thể đảm bảo rằng tất cả các hàm tạo của lớp cha mẹ được gọi (và theo đúng thứ tự và chỉ một lần) không?

Chỉnh sửa: để xem ý tôi là gì, nếu tôi làm:

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

Sau đó tôi nhận được:

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

Lưu ý rằng Binit của được gọi hai lần. Nếu tôi làm:

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

Sau đó tôi nhận được:

Entering C
Entering A
Leaving A
Leaving C

Lưu ý rằng Binit không bao giờ được gọi. Vì vậy, có vẻ như trừ khi tôi biết / kiểm soát các init mà tôi thừa kế từ ( AB) tôi không thể đưa ra lựa chọn an toàn cho lớp tôi đang viết ( C).


Câu trả lời:


78

Cả hai cách đều hoạt động tốt. Cách tiếp cận sử dụng super()dẫn đến tính linh hoạt cao hơn cho các lớp con.

Trong cách tiếp cận cuộc gọi trực tiếp, C.__init__có thể gọi cả hai A.__init__B.__init__.

Khi sử dụng super(), các lớp cần phải được thiết kế để hợp tác nhiều kế thừa trong đó Ccác cuộc gọi super, gọi Amã của nó cũng sẽ gọi mã supernào gọi Bmã đó. Xem http://rhettinger.wordpress.com/2011/05/26/super-considered-super để biết thêm chi tiết về những gì có thể được thực hiện với super.

[Câu hỏi trả lời như được chỉnh sửa sau đó]

Vì vậy, dường như trừ khi tôi biết / kiểm soát các init mà tôi thừa kế từ (A và B), tôi không thể đưa ra lựa chọn an toàn cho lớp tôi đang viết (C).

Bài viết được tham khảo cho thấy cách xử lý tình huống này bằng cách thêm một lớp bao bọc xung quanh AB. Có một ví dụ được thực hiện trong phần có tiêu đề "Cách kết hợp một lớp không hợp tác".

Mọi người có thể muốn rằng việc thừa kế dễ dàng hơn, cho phép bạn dễ dàng soạn các lớp Xe và Máy bay để có được FlyingCar, nhưng thực tế là các thành phần được thiết kế riêng biệt thường cần bộ điều hợp hoặc trình bao bọc trước khi khớp với nhau một cách liền mạch như chúng ta muốn :-)

Một suy nghĩ khác: nếu bạn không hài lòng với chức năng soạn thảo bằng cách sử dụng nhiều kế thừa, bạn có thể sử dụng bố cục để kiểm soát hoàn toàn phương thức nào được gọi vào dịp nào.


4
Không, họ không. Nếu init của B không gọi super, thì init của B sẽ không được gọi nếu chúng ta thực hiện super().__init__()phương pháp này. Nếu tôi gọi A.__init__()B.__init__()trực tiếp, thì (nếu A và B gọi super) tôi sẽ nhận được init của B được gọi nhiều lần.
Adam Parkin

3
@AdamParkin (liên quan đến câu hỏi của bạn khi được chỉnh sửa): Nếu một trong các lớp cha không được thiết kế để sử dụng với super () , thì nó thường có thể được gói theo cách thêm siêu cuộc gọi. Bài viết được tham chiếu cho thấy một ví dụ đã được giải quyết trong phần có tiêu đề "Cách kết hợp một lớp không hợp tác".
Raymond Hettinger

1
Bằng cách nào đó tôi đã bỏ lỡ phần đó khi tôi đọc bài viết. Chính xác những gì tôi đang tìm kiếm. Cảm ơn!
Adam Parkin

1
Nếu bạn đang viết python (hy vọng là 3!) Và sử dụng tính kế thừa của bất kỳ loại nào, nhưng đặc biệt là nhiều, thì rhettinger.wordpress.com/2011/05/26/super-considered-super nên đọc.
Shawn Mehan

1
Nâng cao bởi vì cuối cùng chúng ta cũng biết tại sao chúng ta không có ô tô bay khi chúng ta chắc chắn rằng chúng ta sẽ có bây giờ.
msouth

64

Câu trả lời cho câu hỏi của bạn phụ thuộc vào một khía cạnh rất quan trọng: Các lớp cơ sở của bạn có được thiết kế cho nhiều kế thừa không?

Có 3 kịch bản khác nhau:

  1. Các lớp cơ sở là không liên quan, các lớp độc lập.

    Nếu các lớp cơ sở của bạn là các thực thể riêng biệt có khả năng hoạt động độc lập và chúng không biết nhau, thì chúng không được thiết kế cho nhiều kế thừa. Thí dụ:

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

    Quan trọng: Lưu ý rằng không phải Foocũng không Bargọi super().__init__()! Đây là lý do tại sao mã của bạn không hoạt động chính xác. Do cách thức kế thừa kim cương hoạt động trong python, các lớp có lớp cơ sở objectkhông nên gọisuper().__init__() . Như bạn đã nhận thấy, làm như vậy sẽ phá vỡ nhiều kế thừa vì cuối cùng bạn gọi một lớp khác __init__chứ không phải object.__init__(). ( Disclaimer: Tránh super().__init__()trong object-subclasses là lời khuyên của chúng tôi và không có nghĩa là thoả thuận về sự đồng thuận trong cộng đồng python Một số người thích để sử dụng. superTrong mỗi lớp, cho rằng bạn luôn có thể viết một bộ chuyển đổi nếu lớp không cư xử như bạn mong đợi.)

    Điều này cũng có nghĩa là bạn không bao giờ nên viết một lớp kế thừa objectvà không có __init__phương thức. Không xác định một __init__phương thức nào có tác dụng tương tự như gọi super().__init__(). Nếu lớp của bạn kế thừa trực tiếp từ object, hãy đảm bảo thêm một hàm tạo trống như vậy:

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

    Dù sao, trong tình huống này, bạn sẽ phải gọi từng hàm tạo cha mẹ bằng tay. Có hai cách để làm điều này:

    • Không có super

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

    Mỗi phương pháp đều có ưu điểm và nhược điểm riêng. Nếu bạn sử dụng super, lớp học của bạn sẽ hỗ trợ tiêm phụ thuộc . Mặt khác, dễ phạm sai lầm hơn. Ví dụ: nếu bạn thay đổi thứ tự FooBar(như class FooBar(Bar, Foo)), bạn sẽ phải cập nhật các supercuộc gọi cho phù hợp. Không có superbạn không phải lo lắng về điều này, và mã dễ đọc hơn nhiều.

  2. Một trong những lớp học là một mixin.

    Một mixin là một lớp được thiết kế để được sử dụng với nhiều kế thừa. Điều này có nghĩa là chúng ta không phải gọi cả hai hàm tạo cha theo cách thủ công, vì mixin sẽ tự động gọi hàm tạo thứ 2 cho chúng ta. Vì lần này chúng ta chỉ phải gọi một hàm tạo duy nhất, nên chúng ta có thể làm như vậy superđể tránh phải mã hóa tên của lớp cha.

    Thí dụ:

    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.

    Các chi tiết quan trọng ở đây là:

    • Mixin gọi super().__init__()và chuyển qua bất kỳ đối số nào nó nhận được.
    • Lớp con kế thừa từ mixin đầu tiên : class FooBar(FooMixin, Bar). Nếu thứ tự của các lớp cơ sở là sai, hàm tạo của mixin sẽ không bao giờ được gọi.
  3. Tất cả các lớp cơ sở được thiết kế để kế thừa hợp tác.

    Các lớp được thiết kế để kế thừa hợp tác rất giống với mixin: Chúng chuyển qua tất cả các đối số không được sử dụng cho lớp tiếp theo. Giống như trước đây, chúng ta chỉ cần gọi super().__init__()và tất cả các hàm tạo cha mẹ sẽ được gọi theo chuỗi.

    Thí dụ:

    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

    Trong trường hợp này, thứ tự của các lớp cha không quan trọng. Chúng tôi có thể kế thừa từ CoopBarđầu tiên và mã vẫn hoạt động như cũ. Nhưng điều đó chỉ đúng vì tất cả các đối số được truyền dưới dạng đối số từ khóa. Sử dụng các đối số vị trí sẽ giúp dễ dàng hiểu sai thứ tự của các đối số, do đó, theo thông lệ, các lớp hợp tác chỉ chấp nhận các đối số từ khóa.

    Đây cũng là một ngoại lệ cho quy tắc tôi đã đề cập trước đó: Cả hai CoopFooCoopBarthừa kế từ object, nhưng họ vẫn gọi super().__init__(). Nếu họ không, sẽ không có thừa kế hợp tác.

Dòng dưới cùng: Việc thực hiện đúng phụ thuộc vào các lớp bạn đang kế thừa.

Hàm tạo là một phần của giao diện chung của lớp. Nếu lớp được thiết kế dưới dạng mixin hoặc để kế thừa hợp tác, thì điều đó phải được ghi lại. Nếu các tài liệu không đề cập đến bất cứ điều gì thuộc loại này, sẽ an toàn khi cho rằng lớp không được thiết kế để hợp tác nhiều kế thừa.


2
Điểm thứ hai của bạn thổi tôi đi. Tôi chỉ từng thấy Mixins ở bên phải của siêu hạng thực tế và nghĩ rằng chúng khá lỏng lẻo và nguy hiểm, vì bạn không thể kiểm tra xem lớp bạn đang trộn có thuộc tính mà bạn đang mong đợi không. Tôi chưa bao giờ nghĩ về việc đưa một vị tướng super().__init__(*args, **kwargs)vào mixin và viết nó trước. Nó làm cho rất nhiều ý nghĩa.
Minix

10

Cách tiếp cận ("kiểu mới" hoặc "kiểu cũ") sẽ hoạt động nếu bạn có quyền kiểm soát mã nguồn cho AB . Nếu không, việc sử dụng một lớp bộ điều hợp có thể là cần thiết.

Mã nguồn có thể truy cập: Sử dụng đúng "phong cách mới"

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

Ở đây, thứ tự độ phân giải phương thức (MRO) ra lệnh như sau:

  • C(A, B)mệnh lệnh Ađầu tiên, sau đó B. MRO là C -> A -> B -> object.
  • super(A, self).__init__()tiếp tục dọc theo chuỗi MRO khởi xướng vào năm C.__init__tới B.__init__.
  • super(B, self).__init__()tiếp tục dọc theo chuỗi MRO khởi xướng vào năm C.__init__tới object.__init__.

Bạn có thể nói rằng trường hợp này được thiết kế cho nhiều kế thừa .

Mã nguồn có thể truy cập: Sử dụng đúng "kiểu cũ"

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

Ở đây, MRO không quan trọng, vì A.__init__B.__init__được gọi một cách rõ ràng. class C(B, A):cũng sẽ làm việc như vậy.

Mặc dù trường hợp này không được "thiết kế" cho nhiều kế thừa theo kiểu mới như trước đây, nhưng vẫn có thể thừa kế nhiều kế thừa.


Bây giờ, điều gì xảy ra nếu ABlà từ thư viện của bên thứ ba - tức là bạn không có quyền kiểm soát mã nguồn cho AB ? Câu trả lời ngắn gọn: Bạn phải thiết kế một lớp bộ điều hợp thực hiện các supercuộc gọi cần thiết , sau đó sử dụng một lớp trống để xác định MRO (xem bài viết của Raymond Hettinger trênsuper - đặc biệt là phần "Cách kết hợp một lớp không hợp tác").

Cha mẹ bên thứ ba: Akhông thực hiện super; Blàm

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

Lớp Adapterthực hiện superđể Ccó thể định nghĩa MRO, sẽ phát huy tác dụng khi super(Adapter, self).__init__()được thực thi.

Và nếu nó là cách khác thì sao?

Cha mẹ bên thứ ba: Athực hiện super; Bkhông làm

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

Mẫu tương tự ở đây, ngoại trừ thứ tự thực hiện được chuyển sang Adapter.__init__; supergọi trước, sau đó gọi rõ ràng. Lưu ý rằng mỗi trường hợp với cha mẹ bên thứ ba yêu cầu một lớp bộ điều hợp duy nhất.

Vì vậy, dường như trừ khi tôi biết / kiểm soát các init mà tôi thừa kế từ ( AB) tôi không thể đưa ra lựa chọn an toàn cho lớp tôi đang viết ( C).

Mặc dù bạn có thể xử lý các trường hợp bạn không kiểm soát mã nguồn ABbằng cách sử dụng lớp bộ điều hợp, nhưng đúng là bạn phải biết cách các init của các lớp cha thực hiện super(nếu có) để làm như vậy.


4

Như Raymond đã nói trong câu trả lời của mình, một cuộc gọi trực tiếp đến A.__init__B.__init__hoạt động tốt, và mã của bạn sẽ có thể đọc được.

Tuy nhiên, nó không sử dụng liên kết thừa kế giữa Cvà các lớp đó. Khai thác liên kết đó mang lại cho bạn sự nhất quán hơn và làm cho việc tái cấu trúc cuối cùng dễ dàng hơn và ít bị lỗi hơn. Một ví dụ về cách làm điều đó:

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
Câu trả lời tốt nhất imho. Tìm thấy điều này đặc biệt hữu ích vì nó là tương lai nhiều hơn
Stephen Ellwood

3

Bài viết này giúp giải thích hợp tác nhiều thừa kế:

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

Nó đề cập đến phương thức hữu ích mro()cho bạn thấy thứ tự giải quyết phương thức. Trong ví dụ thứ 2 của bạn, nơi bạn gọi superđến A, supercuộc gọi tiếp tục trong MRO. Lớp tiếp theo theo thứ tự là B, đây là lý do tại sao Binit được gọi lần đầu tiên.

Đây là một bài viết kỹ thuật hơn từ trang web python chính thức:

http://www.python.org/doad/release/2.3/mro/


2

Nếu bạn đang nhân các lớp phân lớp phụ từ các thư viện của bên thứ ba, thì không, không có cách tiếp cận mù nào để gọi các __init__phương thức lớp cơ sở (hoặc bất kỳ phương thức nào khác) thực sự hoạt động bất kể các lớp cơ sở được lập trình như thế nào.

superlàm cho nó có thể viết các lớp được thiết kế để hợp tác thực hiện các phương thức như là một phần của nhiều cây thừa kế phức tạp mà tác giả lớp không cần biết. Nhưng không có cách nào để sử dụng nó để kế thừa chính xác từ các lớp tùy ý có thể hoặc không thể sử dụng super.

Về cơ bản, cho dù một lớp được thiết kế để được phân lớp bằng cách sử dụng superhoặc với các cuộc gọi trực tiếp đến lớp cơ sở là một thuộc tính của "giao diện chung" của lớp, và nó phải được ghi lại như vậy. Nếu bạn đang sử dụng thư viện của bên thứ ba theo cách mà tác giả thư viện mong đợi và thư viện có tài liệu hợp lý, thông thường nó sẽ cho bạn biết bạn cần phải làm gì để phân lớp những thứ cụ thể. Nếu không, thì bạn sẽ phải xem mã nguồn cho các lớp bạn đang phân lớp và xem quy ước gọi lớp cơ sở của chúng là gì. Nếu bạn đang kết hợp nhiều lớp từ các thư viện một hoặc nhiều bên thứ ba trong một cách mà các tác giả thư viện đã không mong đợi, sau đó nó có thể không thể để liên tục gọi các phương pháp siêu lớp ở tất cả các; nếu lớp A là một phần của hệ thống phân cấp sử dụng supervà lớp B là một phần của hệ thống phân cấp không sử dụng siêu, thì không có tùy chọn nào được đảm bảo để hoạt động. Bạn sẽ phải tìm ra một chiến lược xảy ra để làm việc cho từng trường hợp cụ thể.


@RaymondHettinger Vâng, bạn đã viết và liên kết đến một bài viết với một số suy nghĩ về câu trả lời của bạn, vì vậy tôi không nghĩ rằng tôi có nhiều điều để thêm vào đó. :) Tôi không nghĩ rằng có thể thích ứng một cách tổng quát bất kỳ lớp không sử dụng siêu nào thành một hệ thống siêu phân cấp; bạn phải đưa ra một giải pháp phù hợp với các lớp cụ thể liên quan.
Ben
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.