Tại sao các phương thức __init__ siêu lớp không tự động được gọi?


155

Tại sao các nhà thiết kế Python quyết định rằng __init__()các phương thức của các lớp con không tự động gọi các __init__()phương thức của siêu lớp của chúng, như trong một số ngôn ngữ khác? Là thành ngữ Pythonic và được đề nghị thực sự như sau?

class Superclass(object):
    def __init__(self):
        print 'Do something'

class Subclass(Superclass):
    def __init__(self):
        super(Subclass, self).__init__()
        print 'Do something else'

2
Bạn có thể viết một trình trang trí để kế thừa __init__phương thức và thậm chí có thể tự động tìm kiếm các lớp con và trang trí chúng.
osa

2
@osa, nghe có vẻ là một ý kiến ​​hay. Bạn có muốn mô tả thêm một chút về phần trang trí?
Diansheng

1
@osa vâng mô tả thêm!
Charlie Parker

Câu trả lời:


163

Sự khác biệt quan trọng giữa của Python __init__và những ngôn ngữ khác constructors__init__không một constructor: nó là một khởi tạo (thực tế xây dựng (nếu có, nhưng, xem sau ;-) là __new__và làm việc hoàn toàn khác nhau một lần nữa). Trong khi xây dựng tất cả các siêu lớp (và, không còn nghi ngờ gì nữa, làm như vậy "trước" bạn tiếp tục xây dựng xuống dưới) rõ ràng là một phần của việc nói rằng bạn đang xây dựng một thể hiện của lớp con, đó rõ ràng không phải là trường hợp để khởi tạo, vì có nhiều trường hợp sử dụng trong đó việc khởi tạo của siêu lớp cần được bỏ qua, thay đổi, kiểm soát - xảy ra, nếu có, "ở giữa" của khởi tạo lớp con, v.v.

Về cơ bản, ủy quyền siêu lớp của trình khởi tạo không tự động trong Python vì những lý do chính xác như vậy, ủy nhiệm đó cũng không tự động đối với bất kỳ phương thức nào khác - và lưu ý rằng các "ngôn ngữ khác" đó không thực hiện ủy quyền siêu hạng tự động cho bất kỳ phương thức khác hoặc ... chỉ dành cho hàm tạo (và nếu có thể, hàm hủy), như tôi đã đề cập, không phải__init__ là của Python . (Hành vi của __new__cũng khá đặc biệt, mặc dù thực sự không liên quan trực tiếp đến câu hỏi của bạn, vì __new__là ví dụ nhà xây dựng một đặc biệt nó không thực sự nhất thiết cần phải xây dựng bất cứ điều gì - hoàn toàn có thể cũng trở về một thể hiện, hoặc thậm chí một tổ chức phi dụ ... rõ ràng Python cung cấp cho bạn rất nhiềukiểm soát cơ học nhiều hơn so với "các ngôn ngữ khác" mà bạn có trong tâm trí, điều này cũng bao gồm việc không có ủy quyền tự động trong __new__chính nó! -).


7
Được nâng cấp bởi vì đối với tôi, lợi ích thực sự là khả năng bạn đề cập để gọi _ init () _ của siêu lớp tại bất kỳ điểm nào trong khởi tạo của lớp con (hoặc hoàn toàn không).
loại

53
-1 cho " __init__không phải là hàm tạo ... hàm tạo thực tế ... là __new__". Như bạn lưu ý, __new__hành xử không có gì giống như một nhà xây dựng từ các ngôn ngữ khác. __init__trên thực tế rất giống nhau (nó được gọi trong quá trình tạo một đối tượng mới, sau khi đối tượng đó được phân bổ, để đặt các biến thành viên trên đối tượng mới) và hầu như luôn là nơi để thực hiện chức năng mà trong các ngôn ngữ khác bạn sẽ đặt vào một nhà xây dựng. Vì vậy, chỉ cần gọi nó là một nhà xây dựng!
Ben

8
Tôi cũng nghĩ rằng đây là một tuyên bố khá vô lý: " xây dựng tất cả các siêu lớp ... rõ ràng là một phần của việc bạn đang xây dựng một thể hiện của lớp con, đó rõ ràng không phải là trường hợp để khởi tạo ". Không có gì trong các từ xây dựng / khởi tạo làm cho điều này "rõ ràng" với bất cứ ai. Và __new__cũng không tự động gọi siêu lớp __new__. Vì vậy, yêu cầu của bạn rằng sự khác biệt chính là việc xây dựng nhất thiết phải liên quan đến việc xây dựng các siêu lớp, trong khi việc khởi tạo không phù hợp với yêu cầu của bạn __new__là nhà xây dựng.
Ben

36
Trên thực tế, từ các tài liệu python cho __init__tại docs.python.org/reference/datamodel.html#basic-customization : "Là một ràng buộc đặc biệt đối với các nhà xây dựng , không có giá trị nào có thể được trả về; "(Nhấn mạnh của tôi). Vì vậy, nó chính thức, __init__là một nhà xây dựng.
Ben

3
Trong thuật ngữ Python / Java, __init__được gọi là hàm tạo. Hàm tạo này là một hàm khởi tạo được gọi sau khi đối tượng đã được xây dựng hoàn chỉnh và được khởi tạo ở trạng thái mặc định bao gồm loại thời gian chạy cuối cùng của nó. Nó không tương đương với các hàm tạo C ++, được gọi trên một đối tượng được phân bổ, gõ tĩnh với trạng thái không xác định. Chúng cũng khác nhau __new__, vì vậy chúng tôi thực sự có ít nhất bốn loại chức năng phân bổ / xây dựng / khởi tạo khác nhau. Ngôn ngữ sử dụng thuật ngữ hỗn hợp, và phần quan trọng là hành vi chứ không phải thuật ngữ.
Elazar

36

Tôi hơi xấu hổ khi mọi người vẹt "Zen of Python", như thể đó là một lời biện minh cho bất cứ điều gì. Đó là một triết lý thiết kế; các quyết định thiết kế cụ thể luôn có thể được giải thích bằng các thuật ngữ cụ thể hơn - và chúng phải, hoặc nếu không thì "Zen of Python" trở thành cái cớ để làm bất cứ điều gì.

Lý do rất đơn giản: bạn không nhất thiết phải xây dựng một lớp dẫn xuất theo cách tương tự như cách bạn xây dựng lớp cơ sở. Bạn có thể có nhiều tham số hơn, ít hơn, chúng có thể theo một thứ tự khác hoặc hoàn toàn không liên quan.

class myFile(object):
    def __init__(self, filename, mode):
        self.f = open(filename, mode)
class readFile(myFile):
    def __init__(self, filename):
        super(readFile, self).__init__(filename, "r")
class tempFile(myFile):
    def __init__(self, mode):
        super(tempFile, self).__init__("/tmp/file", mode)
class wordsFile(myFile):
    def __init__(self, language):
        super(wordsFile, self).__init__("/usr/share/dict/%s" % language, "r")

Điều này áp dụng cho tất cả các phương pháp dẫn xuất, không chỉ __init__.


6
Liệu ví dụ này cung cấp một cái gì đó đặc biệt? ngôn ngữ tĩnh cũng có thể làm điều này
jean

18

Java và C ++ yêu cầu một hàm tạo của lớp cơ sở được gọi vì bố trí bộ nhớ.

Nếu bạn có một lớp BaseClassvới một thành viên field1và bạn tạo một lớp mới SubClasscó thêm một thành viên field2, thì một thể hiện SubClasschứa không gian cho field1field2. Bạn cần một hàm tạo BaseClassđể điền vào field1, trừ khi bạn yêu cầu tất cả các lớp kế thừa lặp lại BaseClasskhởi tạo trong các hàm tạo của riêng chúng. Và nếu field1là riêng tư, thì kế thừa các lớp không thể khởi tạo field1.

Python không phải là Java hay C ++. Tất cả các phiên bản của tất cả các lớp do người dùng định nghĩa có cùng "hình dạng". Về cơ bản chúng chỉ là từ điển trong đó các thuộc tính có thể được chèn vào. Trước khi bất kỳ khởi tạo nào được thực hiện, tất cả các phiên bản của tất cả các lớp do người dùng định nghĩa gần như giống hệt nhau ; chúng chỉ là nơi lưu trữ các thuộc tính chưa lưu trữ.

Vì vậy, nó có ý nghĩa hoàn hảo cho một lớp con Python không gọi hàm tạo của lớp cơ sở của nó. Nó chỉ có thể thêm các thuộc tính chính nó nếu nó muốn. Không có không gian dành riêng cho một số trường nhất định cho mỗi lớp trong cấu trúc phân cấp và không có sự khác biệt giữa một thuộc tính được thêm bởi mã từ một BaseClassphương thức và một thuộc tính được thêm bởi mã từ một SubClassphương thức.

Nếu, như thông thường, SubClassthực sự muốn có tất cả các BaseClassbất biến được thiết lập trước khi nó tiếp tục thực hiện tùy chỉnh riêng của mình, thì có, bạn chỉ có thể gọi BaseClass.__init__()(hoặc sử dụng super, nhưng đôi khi nó phức tạp và có vấn đề riêng). Nhưng bạn không cần phải làm vậy. Và bạn có thể làm điều đó trước, hoặc sau, hoặc với các đối số khác nhau. Chết tiệt, nếu bạn muốn bạn có thể gọi BaseClass.__init__từ phương thức khác hoàn toàn hơn __init__; có thể bạn có một số điều khởi tạo lười biếng kỳ lạ đi.

Python đạt được sự linh hoạt này bằng cách giữ cho mọi thứ đơn giản. Bạn khởi tạo các đối tượng bằng cách viết một __init__phương thức đặt thuộc tính self. Đó là nó. Nó hành xử chính xác như một phương thức, bởi vì nó chính xác là một phương thức. Không có quy tắc kỳ lạ và không trực quan nào khác về những việc phải được thực hiện trước, hoặc những điều sẽ tự động xảy ra nếu bạn không làm những việc khác. Mục đích duy nhất mà nó cần phục vụ là trở thành một cái móc để thực thi trong quá trình khởi tạo đối tượng để đặt các giá trị thuộc tính ban đầu và nó thực hiện điều đó. Nếu bạn muốn nó làm một cái gì đó khác, bạn viết rõ ràng nó trong mã của bạn.


2
Đối với C ++, nó không liên quan gì đến "bố cục bộ nhớ". Mô hình khởi tạo giống như các ưu đãi của Python có thể đã được triển khai trong C ++. Lý do duy nhất tại sao xây dựng / phá hủy là cách chúng tồn tại trong C ++ là do quyết định thiết kế cung cấp các cơ sở hoạt động tốt và đáng tin cậy để quản lý tài nguyên (RAII), cũng có thể được tạo tự động (nghĩa là ít lỗi mã và con người hơn) bởi trình biên dịch vì các quy tắc cho chúng (thứ tự cuộc gọi của chúng) được xác định chặt chẽ. Không chắc chắn nhưng Java rất có thể chỉ đơn giản là theo cách tiếp cận này để trở thành một ngôn ngữ giống như Pola C khác.
Alexander Shukaev

10

"Rõ ràng là tốt hơn so với ngầm." Đó là lý do tương tự cho thấy chúng ta nên viết rõ ràng 'bản thân'.

Tôi nghĩ cuối cùng nó là một lợi ích-- bạn có thể đọc thuộc tất cả các quy tắc mà Java có liên quan đến việc gọi các nhà xây dựng của siêu lớp không?


8
Tôi đồng ý với bạn về phần lớn, nhưng quy tắc của Java thực sự khá đơn giản: hàm tạo không có đối số được gọi trừ khi bạn đặc biệt yêu cầu một quy tắc khác.
Laurence Gonsalves

1
@Laurence - Điều gì xảy ra khi lớp cha không định nghĩa hàm tạo không có đối số? Điều gì xảy ra khi hàm tạo không có đối số được bảo vệ hoặc riêng tư?
Mike Axiak

8
Chính xác điều tương tự sẽ xảy ra nếu bạn cố gắng gọi nó một cách rõ ràng.
Laurence Gonsalves

8

Thường thì lớp con có các tham số bổ sung không thể truyền cho lớp cha.


7

Ngay bây giờ, chúng tôi có một trang khá dài mô tả thứ tự giải quyết phương thức trong trường hợp có nhiều kế thừa: http://www.python.org/doad/release/2.3/mro/

Nếu các nhà xây dựng được gọi tự động, bạn sẽ cần một trang khác có cùng độ dài giải thích thứ tự xảy ra. Đó sẽ là địa ngục ...


Đây là câu trả lời đúng. Python sẽ cần xác định ngữ nghĩa của đối số chuyển giữa lớp con và siêu lớp. Quá tệ câu trả lời này không được nêu lên. Có lẽ nếu nó cho thấy vấn đề với một số ví dụ?
Gary Weiss

5

Để tránh nhầm lẫn, thật hữu ích khi biết rằng bạn có thể gọi __init__()phương thức base_group nếu child_group không có __init__()lớp.

Thí dụ:

class parent:
  def __init__(self, a=1, b=0):
    self.a = a
    self.b = b

class child(parent):
  def me(self):
    pass

p = child(5, 4)
q = child(7)
z= child()

print p.a # prints 5
print q.b # prints 0
print z.a # prints 1

Trong thực tế, MRO trong python sẽ tìm kiếm __init__()trong lớp cha khi không thể tìm thấy nó trong lớp con. Bạn cần gọi trực tiếp hàm tạo của lớp cha nếu bạn đã có một __init__()phương thức trong lớp con.

Ví dụ: đoạn mã sau sẽ trả về lỗi: class Parent : def init (self, a = 1, b = 0): self.a = a self.b = b

    class child(parent):
      def __init__(self):
        pass
      def me(self):
        pass

    p = child(5, 4) # Error: constructor gets one argument 3 is provided.
    q = child(7)  # Error: constructor gets one argument 2 is provided.

    z= child()
    print z.a # Error: No attribute named as a can be found.

3

Có lẽ __init__là phương thức mà lớp con cần ghi đè. Đôi khi các lớp con cần chức năng của cha mẹ để chạy trước khi chúng thêm mã cụ thể của lớp và những lần khác chúng cần thiết lập các biến thể hiện trước khi gọi hàm của cha mẹ. Vì không có cách nào Python có thể biết khi nào nên gọi các hàm đó là phù hợp nhất, nên không nên đoán.

Nếu những điều đó không ảnh hưởng đến bạn, hãy coi đó __init__chỉ là một chức năng khác. Nếu hàm trong câu hỏi dostuffthay vào đó, bạn vẫn muốn Python tự động gọi hàm tương ứng trong lớp cha chứ?


2

Tôi tin rằng một cân nhắc rất quan trọng ở đây là với một cuộc gọi tự động super.__init__(), bạn đăng ký, theo thiết kế, khi phương thức khởi tạo đó được gọi và với các đối số. eschewing tự động gọi nó và yêu cầu lập trình viên thực hiện cuộc gọi đó một cách rõ ràng, đòi hỏi rất nhiều tính linh hoạt.

xét cho cùng, chỉ vì lớp B có nguồn gốc từ lớp A không có nghĩa là A.__init__()có thể hoặc nên được gọi với cùng các đối số như B.__init__(). làm cho cuộc gọi rõ ràng có nghĩa là một lập trình viên có thể có ví dụ xác định B.__init__()với các tham số hoàn toàn khác nhau, thực hiện một số tính toán với dữ liệu đó, gọi A.__init__()với các đối số phù hợp với phương thức đó và sau đó thực hiện một số xử lý hậu kỳ. loại linh hoạt này sẽ rất khó để đạt được nếu A.__init__()được gọi từ B.__init__()ngầm, trước khi B.__init__()thực hiện hoặc ngay sau nó.

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.