9. Lớp học

Các lớp cung cấp một phương tiện để kết hợp dữ liệu và chức năng lại với nhau. Việc tạo một lớp mới sẽ tạo ra một đối tượng type mới, cho phép tạo instances mới thuộc loại đó. Mỗi thể hiện của lớp có thể có các thuộc tính gắn liền với nó để duy trì trạng thái của nó. Các thể hiện của lớp cũng có thể có các phương thức (được định nghĩa bởi lớp của nó) để sửa đổi trạng thái của nó.

So với các ngôn ngữ lập trình khác, cơ chế lớp của Python bổ sung thêm các lớp với ít cú pháp và ngữ nghĩa mới nhất. Nó là sự kết hợp của các cơ chế lớp được tìm thấy trong C++ và Modula-3. Các lớp Python cung cấp tất cả các tính năng tiêu chuẩn của Lập trình hướng đối tượng: cơ chế kế thừa lớp cho phép nhiều lớp cơ sở, một lớp dẫn xuất có thể ghi đè bất kỳ phương thức nào của lớp cơ sở hoặc các lớp của nó và một phương thức có thể gọi phương thức của lớp cơ sở có cùng tên. Các đối tượng có thể chứa số lượng và loại dữ liệu tùy ý. Đúng như đối với các mô-đun, các lớp có tính chất động của Python: chúng được tạo trong thời gian chạy và có thể được sửa đổi thêm sau khi tạo.

Trong thuật ngữ C++, thông thường các thành viên lớp (bao gồm cả các thành viên dữ liệu) là public (ngoại trừ xem Biến riêng bên dưới) và tất cả các hàm thành viên là virtual. Như trong Modula-3, không có cách viết tắt nào để tham chiếu các thành viên của đối tượng từ các phương thức của nó: hàm phương thức được khai báo với đối số đầu tiên rõ ràng đại diện cho đối tượng, được cung cấp ngầm bởi lệnh gọi. Như trong Smalltalk, bản thân các lớp cũng là đối tượng. Điều này cung cấp ngữ nghĩa cho việc nhập và đổi tên. Không giống như C++ và Modula-3, các kiểu tích hợp sẵn có thể được sử dụng làm lớp cơ sở để người dùng mở rộng. Ngoài ra, giống như trong C++, hầu hết các toán tử tích hợp sẵn với cú pháp đặc biệt (toán tử số học, chỉ số dưới, v.v.) đều có thể được xác định lại cho các thể hiện của lớp.

(Thiếu thuật ngữ được chấp nhận rộng rãi để nói về các lớp, tôi sẽ thỉnh thoảng sử dụng các thuật ngữ Smalltalk và C++. Tôi sẽ sử dụng thuật ngữ Modula-3, vì ngữ nghĩa hướng đối tượng của nó gần với ngữ nghĩa của Python hơn C++, nhưng tôi hy vọng rằng ít độc giả đã nghe nói về nó.)

9.1. Một Lời Về Tên và Đồ Vật

Các đối tượng có tính cá nhân và nhiều tên (trong nhiều phạm vi) có thể được liên kết với cùng một đối tượng. Điều này được gọi là bí danh trong các ngôn ngữ khác. Điều này thường không được đánh giá cao ngay từ cái nhìn đầu tiên về Python và có thể được bỏ qua một cách an toàn khi xử lý các loại cơ bản bất biến (số, chuỗi, bộ dữ liệu). Tuy nhiên, bí danh có thể có tác động đáng ngạc nhiên đối với ngữ nghĩa của mã Python liên quan đến các đối tượng có thể thay đổi như danh sách, từ điển và hầu hết các loại khác. Điều này thường được sử dụng vì lợi ích của chương trình, vì bí danh hoạt động giống như con trỏ ở một số khía cạnh. Ví dụ, việc truyền một đối tượng sẽ rẻ vì chỉ có một con trỏ được truyền trong quá trình triển khai; và nếu một hàm sửa đổi một đối tượng được truyền dưới dạng đối số, thì lệnh gọi sẽ thấy sự thay đổi --- điều này giúp loại bỏ sự cần thiết của hai cơ chế truyền đối số khác nhau như trong Pascal.

9.2. Phạm vi và không gian tên Python

Trước khi giới thiệu các lớp, trước tiên tôi phải nói cho bạn đôi điều về các quy tắc phạm vi của Python. Các định nghĩa lớp sử dụng một số thủ thuật hay với các không gian tên và bạn cần biết phạm vi và không gian tên hoạt động như thế nào để hiểu đầy đủ những gì đang diễn ra. Ngẫu nhiên, kiến ​​thức về chủ đề này rất hữu ích cho bất kỳ lập trình viên Python nâng cao nào.

Hãy bắt đầu với một số định nghĩa.

Zz002zz là ánh xạ từ tên đến đối tượng. Hầu hết các không gian tên hiện được triển khai dưới dạng từ điển Python, nhưng điều đó thường không được chú ý theo bất kỳ cách nào (ngoại trừ hiệu suất) và nó có thể thay đổi trong tương lai. Ví dụ về các không gian tên là: tập hợp các tên dựng sẵn (chứa các hàm như abs() và các tên ngoại lệ dựng sẵn); tên chung trong một mô-đun; và tên cục bộ trong lời gọi hàm. Theo một nghĩa nào đó, tập hợp các thuộc tính của một đối tượng cũng tạo thành một không gian tên. Điều quan trọng cần biết về các không gian tên là hoàn toàn không có mối quan hệ nào giữa các tên trong các không gian tên khác nhau; ví dụ: hai mô-đun khác nhau có thể đều xác định hàm maximize mà không nhầm lẫn --- người dùng mô-đun phải thêm tiền tố vào tên mô-đun.

Nhân tiện, tôi sử dụng từ attribute cho bất kỳ tên nào theo sau dấu chấm --- ví dụ: trong biểu thức z.real, real là một thuộc tính của đối tượng z. Nói đúng ra, tham chiếu đến tên trong mô-đun là tham chiếu thuộc tính: trong biểu thức modname.funcname, modname là một đối tượng mô-đun và funcname là một thuộc tính của nó. Trong trường hợp này, có một sự ánh xạ đơn giản giữa các thuộc tính của mô-đun và tên chung được xác định trong mô-đun: chúng chia sẻ cùng một không gian tên! [1]

Các thuộc tính có thể chỉ đọc hoặc có thể ghi. Trong trường hợp sau, có thể gán cho các thuộc tính. Thuộc tính mô-đun có thể ghi được: bạn có thể viết modname.the_answer = 42. Các thuộc tính có thể ghi cũng có thể bị xóa bằng câu lệnh del. Ví dụ: del modname.the_answer sẽ xóa thuộc tính the_answer khỏi đối tượng được đặt tên bởi modname.

Không gian tên được tạo vào những thời điểm khác nhau và có thời gian tồn tại khác nhau. Không gian tên chứa các tên dựng sẵn được tạo khi trình thông dịch Python khởi động và không bao giờ bị xóa. Không gian tên chung cho một mô-đun được tạo khi định nghĩa mô-đun được đọc vào; thông thường, không gian tên mô-đun cũng tồn tại cho đến khi trình thông dịch thoát. Các câu lệnh được thực thi bởi lệnh gọi cấp cao nhất của trình thông dịch, được đọc từ tệp tập lệnh hoặc tương tác, được coi là một phần của mô-đun có tên __main__, vì vậy chúng có không gian tên chung của riêng mình. (Các tên cài sẵn thực tế cũng tồn tại trong một mô-đun; tên này được gọi là builtins.)

Không gian tên cục bộ cho một hàm được tạo khi hàm được gọi và bị xóa khi hàm trả về hoặc đưa ra một ngoại lệ không được xử lý trong hàm. (Trên thực tế, việc quên sẽ là cách tốt hơn để mô tả những gì thực sự xảy ra.) Tất nhiên, mỗi lời gọi đệ quy đều có không gian tên cục bộ riêng.

Zz000zz là vùng văn bản của chương trình Python nơi có thể truy cập trực tiếp không gian tên. "Có thể truy cập trực tiếp" ở đây có nghĩa là tham chiếu không đủ tiêu chuẩn đến một tên sẽ cố gắng tìm tên đó trong không gian tên.

Mặc dù phạm vi được xác định tĩnh nhưng chúng được sử dụng động. Tại bất kỳ thời điểm nào trong quá trình thực thi, có 3 hoặc 4 phạm vi lồng nhau có không gian tên có thể truy cập trực tiếp:

  • phạm vi trong cùng, được tìm kiếm đầu tiên, chứa tên cục bộ

  • phạm vi của bất kỳ hàm kèm theo nào, được tìm kiếm bắt đầu bằng phạm vi bao quanh gần nhất, chứa các tên không cục bộ nhưng cũng không phải toàn cục

  • phạm vi tiếp theo chứa tên toàn cầu của mô-đun hiện tại

  • phạm vi ngoài cùng (được tìm kiếm cuối cùng) là không gian tên chứa các tên có sẵn

Nếu một tên được khai báo toàn cục thì tất cả các tham chiếu và phép gán sẽ chuyển trực tiếp đến phạm vi tiếp theo chứa tên toàn cục của mô-đun. Để liên kết lại các biến được tìm thấy bên ngoài phạm vi trong cùng, có thể sử dụng câu lệnh nonlocal; nếu không được khai báo là không cục bộ, các biến đó sẽ ở chế độ chỉ đọc (cố gắng ghi vào một biến như vậy sẽ chỉ tạo một biến cục bộ new trong phạm vi trong cùng, giữ nguyên biến bên ngoài có tên giống hệt nhau).

Thông thường, phạm vi cục bộ tham chiếu đến tên cục bộ của hàm hiện tại (theo văn bản). Các hàm bên ngoài, phạm vi cục bộ tham chiếu cùng một không gian tên với phạm vi toàn cục: không gian tên của mô-đun. Các định nghĩa lớp đặt một không gian tên khác vào phạm vi cục bộ.

Điều quan trọng là phải nhận ra rằng phạm vi được xác định theo văn bản: phạm vi toàn cục của một hàm được xác định trong một mô-đun là không gian tên của mô-đun đó, bất kể hàm đó được gọi từ đâu hoặc bằng bí danh nào. Mặt khác, việc tìm kiếm tên thực tế được thực hiện một cách linh hoạt, trong thời gian chạy --- tuy nhiên, định nghĩa ngôn ngữ đang phát triển theo hướng phân giải tên tĩnh, tại thời điểm "biên dịch", vì vậy đừng dựa vào độ phân giải tên động! (Trên thực tế, các biến cục bộ đã được xác định tĩnh.)

Một điểm đặc biệt của Python là -- nếu không có câu lệnh global hoặc nonlocal nào có hiệu lực -- việc gán tên luôn đi vào phạm vi trong cùng. Bài tập không sao chép dữ liệu --- chúng chỉ liên kết tên với các đối tượng. Điều tương tự cũng đúng đối với việc xóa: câu lệnh del x loại bỏ ràng buộc x khỏi không gian tên được tham chiếu bởi phạm vi cục bộ. Trên thực tế, tất cả các hoạt động giới thiệu tên mới đều sử dụng phạm vi cục bộ: cụ thể là các câu lệnh import và định nghĩa hàm liên kết tên mô-đun hoặc hàm trong phạm vi cục bộ.

Câu lệnh global có thể được sử dụng để chỉ ra rằng các biến cụ thể nằm trong phạm vi toàn cục và sẽ được phục hồi ở đó; câu lệnh nonlocal chỉ ra rằng các biến cụ thể nằm trong phạm vi kèm theo và sẽ được phục hồi ở đó.

9.2.1. Ví dụ về phạm vi và không gian tên

Đây là ví dụ minh họa cách tham chiếu các phạm vi và không gian tên khác nhau cũng như cách globalnonlocal ảnh hưởng đến liên kết biến:

def phạm vi_test():
    chắc chắn do_local():
        thư rác = "thư rác cục bộ"

    chắc chắn do_nonlocal():
        thư rác không cục bộ
        spam = "thư rác không cục bộ"

    chắc chắn do_global():
        thư rác toàn cầu
        thư rác = "thư rác toàn cầu"

    thư rác = "kiểm tra thư rác"
    do_local()
    print("Sau khi gán cục bộ:", spam)
    do_nonlocal()
    print("Sau khi gán nonlocal:", spam)
    do_global()
    print("Sau khi gán toàn cục:", spam)

phạm vi_test()
print("Trong phạm vi toàn cục:", spam)

Đầu ra của mã ví dụ là:

Sau khi phân công cục bộ: kiểm tra thư rác
Sau khi chuyển nhượng không cục bộ: thư rác không cục bộ
Sau khi phân công toàn cầu: thư rác không cục bộ
Trong phạm vi toàn cầu: thư rác toàn cầu

Lưu ý cách gán local (mặc định) không thay đổi liên kết của scope_test với spam. Phép gán nonlocal đã thay đổi liên kết của scope_testvới spam và phép gán global đã thay đổi liên kết cấp mô-đun.

Bạn cũng có thể thấy rằng không có ràng buộc nào trước đó cho spam trước khi gán global.

9.3. Cái nhìn đầu tiên về lớp học

Các lớp giới thiệu một chút cú pháp mới, ba loại đối tượng mới và một số ngữ nghĩa mới.

9.3.1. Cú pháp định nghĩa lớp

Dạng định nghĩa lớp đơn giản nhất trông như thế này:

lớp Tên lớp:
    <câu lệnh-1>
    .
    .
    .
    <câu lệnh-N>

Các định nghĩa lớp, như định nghĩa hàm (câu lệnh def) phải được thực thi trước khi chúng có bất kỳ hiệu lực nào. (Bạn có thể hình dung rằng bạn có thể đặt định nghĩa lớp trong một nhánh của câu lệnh if hoặc bên trong một hàm.)

Trong thực tế, các câu lệnh bên trong một định nghĩa lớp thường sẽ là các định nghĩa hàm, nhưng các câu lệnh khác được cho phép và đôi khi hữu ích --- chúng ta sẽ quay lại vấn đề này sau. Các định nghĩa hàm bên trong một lớp thường có một dạng danh sách đối số đặc biệt, được quy định bởi các quy ước gọi phương thức --- một lần nữa, điều này sẽ được giải thích sau.

Khi một định nghĩa lớp được nhập, một không gian tên mới sẽ được tạo và được sử dụng làm phạm vi cục bộ --- do đó, tất cả các phép gán cho các biến cục bộ đều đi vào không gian tên mới này. Đặc biệt, các định nghĩa hàm liên kết tên của hàm mới ở đây.

Khi định nghĩa lớp được giữ nguyên bình thường (đến cuối), class object sẽ được tạo. Về cơ bản, đây là một trình bao bọc xung quanh nội dung của không gian tên được tạo bởi định nghĩa lớp; chúng ta sẽ tìm hiểu thêm về các đối tượng lớp trong phần tiếp theo. Phạm vi cục bộ ban đầu (phạm vi có hiệu lực ngay trước khi nhập định nghĩa lớp) được khôi phục và đối tượng lớp được liên kết ở đây với tên lớp được đưa ra trong tiêu đề định nghĩa lớp (trong ví dụ là ClassName).

9.3.2. Đối tượng lớp

Các đối tượng lớp hỗ trợ hai loại hoạt động: tham chiếu thuộc tính và khởi tạo.

Attribute references sử dụng cú pháp tiêu chuẩn được sử dụng cho tất cả các tham chiếu thuộc tính trong Python: obj.name. Tên thuộc tính hợp lệ là tất cả các tên có trong không gian tên của lớp khi đối tượng lớp được tạo. Vì vậy, nếu định nghĩa lớp trông như thế này

lớp MyClass:
    """Một lớp ví dụ đơn giản"""
    tôi = 12345

    def f(tự):
        trở lại 'xin chào thế giới'

thì MyClass.iMyClass.f là các tham chiếu thuộc tính hợp lệ, trả về một số nguyên và một đối tượng hàm tương ứng. Các thuộc tính lớp cũng có thể được gán cho, vì vậy bạn có thể thay đổi giá trị của MyClass.i bằng cách gán. __doc__ cũng là một thuộc tính hợp lệ, trả về chuỗi tài liệu thuộc lớp: "A simple example class".

Lớp instantiation sử dụng ký hiệu hàm. Chỉ cần giả vờ rằng đối tượng lớp là một hàm không tham số trả về một thể hiện mới của lớp. Ví dụ (giả sử lớp trên):

x = MyClass()

tạo một instance mới của lớp và gán đối tượng này cho biến cục bộ x.

Thao tác khởi tạo ("gọi" một đối tượng lớp) tạo ra một đối tượng trống. Nhiều lớp thích tạo các đối tượng với các thể hiện được tùy chỉnh theo trạng thái ban đầu cụ thể. Vì vậy, một lớp có thể định nghĩa một phương thức đặc biệt có tên là __init__(), như thế này:

định nghĩa __init__(tự):
    self.data = []

Khi một lớp định nghĩa một phương thức __init__(), việc khởi tạo lớp sẽ tự động gọi __init__() cho phiên bản lớp mới được tạo. Vì vậy, trong ví dụ này, một phiên bản mới, được khởi tạo có thể được lấy bằng cách:

x = MyClass()

Tất nhiên, phương pháp __init__() có thể có các đối số để có tính linh hoạt cao hơn. Trong trường hợp đó, các đối số được cung cấp cho toán tử khởi tạo lớp sẽ được chuyển tới __init__(). Ví dụ,

>>> Lớp phức tạp:
... def __init__(self, realpart, imagepart):
... self.r = phần thực
... self.i = phần hình ảnh
...
>>> x = Phức(3.0, -4.5)
>>> x.r, x.i
(3,0, -4,5)

9.3.3. Đối tượng sơ thẩm

Bây giờ chúng ta có thể làm gì với các đối tượng instance? Các hoạt động duy nhất được các đối tượng thể hiện hiểu là các tham chiếu thuộc tính. Có hai loại tên thuộc tính hợp lệ: thuộc tính dữ liệu và phương thức.

Data attributes tương ứng với "biến thể hiện" trong Smalltalk và "thành viên dữ liệu" trong C++. Thuộc tính dữ liệu không cần phải khai báo; giống như các biến cục bộ, chúng xuất hiện khi được gán lần đầu tiên. Ví dụ: nếu x là phiên bản của MyClass được tạo ở trên, đoạn mã sau sẽ in giá trị 16 mà không để lại dấu vết:

x.bộ đếm = 1
trong khi x.counter < 10:
    x.counter = x.counter * 2
in(x.counter)
del x.counter

Loại tham chiếu thuộc tính cá thể khác là method. Phương thức là một hàm "thuộc về" một đối tượng.

Tên phương thức hợp lệ của một đối tượng thể hiện phụ thuộc vào lớp của nó. Theo định nghĩa, tất cả các thuộc tính của một lớp là các đối tượng hàm đều xác định các phương thức tương ứng của các thể hiện của nó. Vì vậy, trong ví dụ của chúng tôi, x.f là một tham chiếu phương thức hợp lệ, vì MyClass.f là một hàm, nhưng x.i thì không, vì MyClass.i thì không. Nhưng x.f không giống với MyClass.f --- nó là method object, không phải là một đối tượng hàm.

9.3.4. Đối tượng phương thức

Thông thường, một phương thức được gọi ngay sau khi nó bị ràng buộc:

x.f()

Nếu x = MyClass() như trên, nó sẽ trả về chuỗi 'hello world'. Tuy nhiên, không cần thiết phải gọi một phương thức ngay lập tức: x.f là một đối tượng phương thức và có thể được lưu trữ và gọi sau. Ví dụ:

xf = x.f
trong khi Đúng:
    in(xf())

sẽ tiếp tục in hello world cho đến hết thời gian.

Chính xác thì điều gì sẽ xảy ra khi một phương thức được gọi? Bạn có thể nhận thấy rằng x.f() được gọi mà không có đối số ở trên, mặc dù định nghĩa hàm cho f() đã chỉ định một đối số. Điều gì đã xảy ra với cuộc tranh luận? Chắc chắn Python đưa ra một ngoại lệ khi một hàm yêu cầu đối số được gọi mà không có bất kỳ đối số nào --- ngay cả khi đối số đó không thực sự được sử dụng...

Trên thực tế, bạn có thể đã đoán được câu trả lời: điều đặc biệt về các phương thức là đối tượng thể hiện được truyền làm đối số đầu tiên của hàm. Trong ví dụ của chúng tôi, lệnh gọi x.f() hoàn toàn tương đương với MyClass.f(x). Nói chung, việc gọi một phương thức có danh sách các đối số n tương đương với việc gọi hàm tương ứng với danh sách đối số được tạo bằng cách chèn đối tượng thể hiện của phương thức trước đối số đầu tiên.

Nói chung, các phương pháp hoạt động như sau. Khi một thuộc tính phi dữ liệu của một thể hiện được tham chiếu, lớp của thể hiện đó sẽ được tìm kiếm. Nếu tên biểu thị một thuộc tính lớp hợp lệ là một đối tượng hàm, thì các tham chiếu đến cả đối tượng thể hiện và đối tượng hàm sẽ được đóng gói vào một đối tượng phương thức. Khi đối tượng phương thức được gọi với danh sách đối số, một danh sách đối số mới được xây dựng từ đối tượng thể hiện và danh sách đối số, đồng thời đối tượng hàm được gọi với danh sách đối số mới này.

9.3.5. Biến lớp và thể hiện

Nói chung, các biến thể hiện dành cho dữ liệu duy nhất cho từng thể hiện và các biến lớp dành cho các thuộc tính và phương thức được chia sẻ bởi tất cả các thể hiện của lớp

lớp chó:

    kind = 'canine' biến # class được chia sẻ bởi tất cả các phiên bản

    def __init__(bản thân, tên):
        self.name = tên biến # instance duy nhất cho mỗi phiên bản

>>> d = Con chó('Fido')
>>> e = Con chó('Buddy')
>>> d.kind # shared bởi tất cả các chú chó
'chó'
>>> e.kind # shared của tất cả các chú chó
'chó'
>>> d.name # unique sang d
'Fido'
>>> e.name # unique gửi e
'Bạn thân'

Như đã thảo luận trong Một Lời Về Tên và Đồ Vật, dữ liệu được chia sẻ có thể có những tác động đáng ngạc nhiên liên quan đến các đối tượng mutable như danh sách và từ điển. Ví dụ: không nên sử dụng danh sách tricks trong đoạn mã sau làm biến lớp vì chỉ một danh sách duy nhất sẽ được chia sẻ bởi tất cả các phiên bản Dog:

lớp chó:

    thủ thuật = [] # mistaken sử dụng biến lớp

    def __init__(bản thân, tên):
        self.name = tên

    def add_trick(tự, lừa):
        self.tricks.append(lừa)

>>> d = Con chó('Fido')
>>> e = Con chó('Buddy')
>>> d.add_trick('cuộn qua')
>>> e.add_trick('chơi chết')
>>> d.tricks # unexpectedly được chia sẻ bởi tất cả các chú chó
['lăn qua', 'chơi chết']

Thiết kế đúng của lớp nên sử dụng một biến thể hiện thay thế

lớp chó:

    def __init__(bản thân, tên):
        self.name = tên
        self.tricks = [] # creates một danh sách trống mới cho mỗi con chó

    def add_trick(tự, lừa):
        self.tricks.append(lừa)

>>> d = Con chó('Fido')
>>> e = Con chó('Buddy')
>>> d.add_trick('cuộn qua')
>>> e.add_trick('chơi chết')
>>> d.tricks
['lăn qua']
>>> thủ thuật điện tử
['chơi chết']

9.4. Nhận xét ngẫu nhiên

Nếu cùng một tên thuộc tính xuất hiện trong cả một phiên bản và một lớp thì việc tra cứu thuộc tính sẽ ưu tiên phiên bản đó:

>>> Kho hạng:
... mục đích = 'lưu trữ'
... vùng = 'tây'
...
>>> w1 = Kho()
>>> in(w1.pure, w1.khu vực)
lưu trữ hướng tây
>>> w2 = Kho()
>>> w2.khu vực = 'phía đông'
>>> in(w2.pure, w2.zone)
lưu trữ đông

Các thuộc tính dữ liệu có thể được tham chiếu bởi các phương thức cũng như bởi người dùng thông thường ("khách hàng") của một đối tượng. Nói cách khác, các lớp không thể sử dụng được để triển khai các kiểu dữ liệu trừu tượng thuần túy. Trên thực tế, không có gì trong Python có thể thực thi việc ẩn dữ liệu --- tất cả đều dựa trên quy ước. (Mặt khác, việc triển khai Python, được viết bằng C, có thể ẩn hoàn toàn các chi tiết triển khai và kiểm soát quyền truy cập vào một đối tượng nếu cần; điều này có thể được sử dụng bởi các phần mở rộng của Python được viết bằng C.)

Khách hàng nên cẩn thận khi sử dụng các thuộc tính dữ liệu --- khách hàng có thể làm hỏng các bất biến được duy trì bởi các phương thức bằng cách dán nhãn lên các thuộc tính dữ liệu của họ. Lưu ý rằng khách hàng có thể thêm các thuộc tính dữ liệu của riêng họ vào một đối tượng thể hiện mà không ảnh hưởng đến tính hợp lệ của các phương thức, miễn là tránh được xung đột tên --- một lần nữa, quy ước đặt tên có thể giúp bạn bớt đau đầu hơn rất nhiều ở đây.

Không có cách viết tắt nào để tham chiếu các thuộc tính dữ liệu (hoặc các phương thức khác!) từ bên trong các phương thức. Tôi thấy rằng điều này thực sự làm tăng khả năng đọc của các phương thức: không có khả năng gây nhầm lẫn giữa các biến cục bộ và biến thể hiện khi lướt qua một phương thức.

Thông thường, đối số đầu tiên của một phương thức được gọi là self. Đây không gì khác hơn là một quy ước: cái tên self hoàn toàn không có ý nghĩa đặc biệt nào đối với Python. Tuy nhiên, hãy lưu ý rằng nếu không tuân theo quy ước, mã của bạn có thể khó đọc hơn đối với các lập trình viên Python khác và cũng có thể hình dung rằng chương trình class browser có thể được viết dựa trên quy ước đó.

Bất kỳ đối tượng hàm nào là thuộc tính lớp đều định nghĩa một phương thức cho các thể hiện của lớp đó. Không cần thiết phải định nghĩa hàm bằng văn bản trong định nghĩa lớp: gán một đối tượng hàm cho một biến cục bộ trong lớp cũng được. Ví dụ:

# Function được xác định bên ngoài lớp
def f1(tự, x, y):
    trả về min(x, x+y)

lớp C:
    f = f1

    def g(tự):
        trở lại 'xin chào thế giới'

    h = g

Bây giờ f, gh đều là các thuộc tính của lớp C tham chiếu đến các đối tượng hàm và do đó chúng đều là phương thức của các thể hiện của C --- h hoàn toàn tương đương với g. Lưu ý rằng cách làm này thường chỉ nhằm mục đích gây nhầm lẫn cho người đọc chương trình.

Các phương thức có thể gọi các phương thức khác bằng cách sử dụng các thuộc tính phương thức của đối số self

túi lớp:
    định nghĩa __init__(tự):
        self.data = []

    def add(self, x):
        self.data.append(x)

    def addtwo(self, x):
        self.add(x)
        self.add(x)

Các phương thức có thể tham chiếu đến tên toàn cục giống như các hàm thông thường. Phạm vi toàn cục được liên kết với một phương thức là mô-đun chứa định nghĩa của nó. (Một lớp không bao giờ được sử dụng như một phạm vi toàn cục.) Mặc dù hiếm khi gặp một lý do chính đáng để sử dụng dữ liệu toàn cục trong một phương thức, nhưng có nhiều cách sử dụng hợp pháp phạm vi toàn cục: có một điều, các hàm và mô-đun được nhập vào phạm vi toàn cục có thể được sử dụng bởi các phương thức cũng như các hàm và lớp được định nghĩa trong đó. Thông thường, lớp chứa phương thức đó tự nó được định nghĩa trong phạm vi toàn cục này và trong phần tiếp theo, chúng ta sẽ tìm thấy một số lý do chính đáng tại sao một phương thức lại muốn tham chiếu đến lớp của chính nó.

Mỗi giá trị là một đối tượng và do đó có class (còn được gọi là type). Nó được lưu trữ dưới dạng object.__class__.

9.5. Kế thừa

Tất nhiên, một đặc điểm ngôn ngữ sẽ không xứng đáng với tên gọi “lớp” nếu không hỗ trợ tính kế thừa. Cú pháp cho một định nghĩa lớp dẫn xuất trông như thế này:

lớp DerivedClassName(BaseClassName):
    <câu lệnh-1>
    .
    .
    .
    <câu lệnh-N>

Tên BaseClassName phải được xác định trong một không gian tên có thể truy cập được từ phạm vi chứa định nghĩa lớp dẫn xuất. Thay cho tên lớp cơ sở, các biểu thức tùy ý khác cũng được cho phép. Điều này có thể hữu ích, ví dụ, khi lớp cơ sở được định nghĩa trong một mô-đun khác:

lớp DerivedClassName(modname.BaseClassName):

Việc thực thi định nghĩa lớp dẫn xuất tiến hành tương tự như đối với lớp cơ sở. Khi đối tượng lớp được xây dựng, lớp cơ sở sẽ được ghi nhớ. Điều này được sử dụng để giải quyết các tham chiếu thuộc tính: nếu không tìm thấy thuộc tính được yêu cầu trong lớp, việc tìm kiếm sẽ tiếp tục tìm trong lớp cơ sở. Quy tắc này được áp dụng đệ quy nếu bản thân lớp cơ sở được dẫn xuất từ ​​một số lớp khác.

Không có gì đặc biệt về việc khởi tạo các lớp dẫn xuất: DerivedClassName() tạo một phiên bản mới của lớp. Các tham chiếu phương thức được giải quyết như sau: thuộc tính lớp tương ứng được tìm kiếm, đi xuống chuỗi các lớp cơ sở nếu cần và tham chiếu phương thức là hợp lệ nếu điều này mang lại một đối tượng hàm.

Các lớp dẫn xuất có thể ghi đè các phương thức của lớp cơ sở của chúng. Bởi vì các phương thức không có đặc quyền khi gọi các phương thức khác của cùng một đối tượng, nên một phương thức của lớp cơ sở gọi một phương thức khác được định nghĩa trong cùng một lớp cơ sở có thể sẽ gọi một phương thức của lớp dẫn xuất ghi đè lên nó. (Đối với người lập trình C++: tất cả các phương thức trong Python đều có hiệu quả là virtual.)

Trên thực tế, một phương thức ghi đè trong lớp dẫn xuất có thể muốn mở rộng hơn là chỉ thay thế phương thức lớp cơ sở có cùng tên. Có một cách đơn giản để gọi trực tiếp phương thức lớp cơ sở: chỉ cần gọi BaseClassName.methodname(self, arguments). Điều này đôi khi cũng hữu ích cho khách hàng. (Lưu ý rằng điều này chỉ hoạt động nếu lớp cơ sở có thể truy cập được dưới dạng BaseClassName trong phạm vi toàn cầu.)

Python có hai hàm dựng sẵn hoạt động với tính kế thừa:

  • Sử dụng isinstance() để kiểm tra loại của một phiên bản: isinstance(obj, int) sẽ chỉ là True nếu obj.__class__int hoặc một số lớp bắt nguồn từ int.

  • Sử dụng issubclass() để kiểm tra tính kế thừa của lớp: issubclass(bool, int)Truebool là lớp con của int. Tuy nhiên, issubclass(float, int)Falsefloat không phải là lớp con của int.

9.5.1. Đa kế thừa

Python cũng hỗ trợ một dạng đa kế thừa. Một định nghĩa lớp với nhiều lớp cơ sở trông như thế này:

lớp DerivedClassName(Base1, Base2, Base3):
    <câu lệnh-1>
    .
    .
    .
    <câu lệnh-N>

Đối với hầu hết các mục đích, trong những trường hợp đơn giản nhất, bạn có thể coi việc tìm kiếm các thuộc tính được kế thừa từ lớp cha là theo chiều sâu, từ trái sang phải, không tìm kiếm hai lần trong cùng một lớp khi có sự chồng chéo trong hệ thống phân cấp. Do đó, nếu một thuộc tính không được tìm thấy trong DerivedClassName, nó sẽ được tìm kiếm trong Base1, sau đó (đệ quy) trong các lớp cơ sở của Base1 và nếu không tìm thấy ở đó, nó sẽ được tìm kiếm trong Base2, v.v.

Trên thực tế, nó phức tạp hơn thế một chút; thứ tự phân giải phương thức thay đổi linh hoạt để hỗ trợ các lệnh gọi hợp tác tới super(). Cách tiếp cận này được biết đến trong một số ngôn ngữ đa kế thừa khác dưới dạng phương thức gọi tiếp theo và mạnh hơn so với siêu lệnh gọi được tìm thấy trong các ngôn ngữ kế thừa đơn.

Thứ tự động là cần thiết vì tất cả các trường hợp đa kế thừa đều thể hiện một hoặc nhiều mối quan hệ kim cương (trong đó ít nhất một trong các lớp cha có thể được truy cập thông qua nhiều đường dẫn từ lớp dưới cùng). Ví dụ: tất cả các lớp kế thừa từ object, do đó, bất kỳ trường hợp đa kế thừa nào cũng cung cấp nhiều đường dẫn để tiếp cận object. Để giữ cho các lớp cơ sở không bị truy cập nhiều lần, thuật toán động tuyến tính hóa thứ tự tìm kiếm theo cách duy trì thứ tự từ trái sang phải được chỉ định trong mỗi lớp, chỉ gọi mỗi lớp cha một lần và đó là đơn điệu (có nghĩa là một lớp có thể được phân lớp mà không ảnh hưởng đến thứ tự ưu tiên của lớp cha). Được kết hợp với nhau, các thuộc tính này giúp thiết kế các lớp đáng tin cậy và có thể mở rộng với nhiều kế thừa. Để biết thêm chi tiết, xem Thứ tự phân giải phương thức Python 2.3.

9.6. Biến riêng

Các biến thể hiện "Riêng tư" không thể truy cập được ngoại trừ từ bên trong một đối tượng không tồn tại trong Python. Tuy nhiên, có một quy ước được hầu hết các mã Python tuân theo: tên có tiền tố gạch dưới (ví dụ: _spam) phải được coi là một phần không công khai của API (cho dù đó là hàm, phương thức hay thành viên dữ liệu). Nó nên được coi là một chi tiết triển khai và có thể thay đổi mà không cần thông báo trước.

Vì có một trường hợp sử dụng hợp lệ cho các thành viên lớp-riêng tư (cụ thể là để tránh xung đột tên giữa các tên với tên được xác định bởi các lớp con), nên có sự hỗ trợ hạn chế cho cơ chế như vậy, được gọi là name mangling. Bất kỳ mã định danh nào có dạng __spam (ít nhất hai dấu gạch dưới ở đầu, nhiều nhất là một dấu gạch dưới ở cuối) đều được thay thế bằng văn bản bằng _classname__spam, trong đó classname là tên lớp hiện tại với (các) dấu gạch dưới ở đầu bị loại bỏ. Việc xáo trộn này được thực hiện mà không quan tâm đến vị trí cú pháp của mã định danh, miễn là nó xảy ra trong định nghĩa của một lớp.

Xem thêm

Zz000zz để biết chi tiết và các trường hợp đặc biệt.

Việc xáo trộn tên rất hữu ích trong việc cho phép các lớp con ghi đè các phương thức mà không làm gián đoạn các lệnh gọi phương thức trong lớp. Ví dụ:

Bản đồ lớp:
    def __init__(tự,  thể lặp lại):
        self.items_list = []
        self.__update( thể lặp lại)

    cập nhật def (tự,  thể lặp lại):
        cho mục trong iterable:
            self.items_list.append(item)

    __update = cập nhật bản sao # private của phương thức update() gốc

lớp MappingSubclass(Bản đồ):

    cập nhật def (tự, khóa, giá trị):
        # provides chữ ký mới để cập nhật()
        # but không phá vỡ __init__()
        đối với mục trong zip (khóa, giá trị):
            self.items_list.append(item)

Ví dụ trên sẽ hoạt động ngay cả khi MappingSubclass giới thiệu mã định danh __update vì nó được thay thế bằng _Mapping__update trong lớp Mapping_MappingSubclass__update trong lớp MappingSubclass.

Lưu ý rằng các quy tắc xáo trộn được thiết kế chủ yếu để tránh tai nạn; vẫn có thể truy cập hoặc sửa đổi một biến được coi là riêng tư. Điều này thậm chí có thể hữu ích trong những trường hợp đặc biệt, chẳng hạn như trong trình gỡ lỗi.

Lưu ý rằng mã được truyền tới exec() hoặc eval() không coi tên lớp của lớp đang gọi là lớp hiện tại; điều này tương tự như tác dụng của câu lệnh global, tác dụng của nó cũng bị hạn chế đối với mã được biên dịch theo byte cùng nhau. Hạn chế tương tự áp dụng cho getattr(), setattr()delattr(), cũng như khi tham chiếu trực tiếp __dict__.

9.7. Tỷ lệ cược và kết thúc

Đôi khi sẽ rất hữu ích khi có kiểu dữ liệu tương tự như "bản ghi" Pascal hoặc "cấu trúc" Pascal, gộp một vài mục dữ liệu được đặt tên lại với nhau. Cách tiếp cận thành ngữ là sử dụng dataclasses cho mục đích này

từ các lớp dữ liệu nhập lớp dữ liệu

@dataclass
Nhân viên lớp:
    tên: str
    phòng: str
    mức lương: int
>>> john = Nhân viên('john', 'phòng thí nghiệm máy tính', 1000)
>>> john.dept
'phòng thí nghiệm máy tính'
>>> john.lương
1000

Một đoạn mã Python yêu cầu một kiểu dữ liệu trừu tượng cụ thể thường có thể được chuyển qua một lớp mô phỏng các phương thức của kiểu dữ liệu đó. Ví dụ: nếu bạn có hàm định dạng một số dữ liệu từ một đối tượng tệp, bạn có thể định nghĩa một lớp bằng các phương thức read()readline() để lấy dữ liệu từ bộ đệm chuỗi và chuyển nó làm đối số.

Instance method objects cũng có các thuộc tính: m.__self__ là đối tượng thể hiện với phương thức m()m.__func__function object tương ứng với phương thức.

9.8. Trình vòng lặp

Đến bây giờ có lẽ bạn đã nhận thấy rằng hầu hết các đối tượng vùng chứa có thể được lặp lại bằng cách sử dụng câu lệnh for

cho phần tử trong [1, 2, 3]:
    in (phần tử)
đối với phần tử trong (1, 2, 3):
    in (phần tử)
cho khóa trong {'one':1, 'two':2}:
    in (khóa)
cho char trong "123":
    in(char)
cho dòng trong open("myfile.txt"):
    in(dòng, kết thúc='')

Phong cách truy cập này rõ ràng, ngắn gọn và thuận tiện. Việc sử dụng các trình vòng lặp lan tỏa và thống nhất Python. Phía sau, câu lệnh for gọi iter() trên đối tượng vùng chứa. Hàm trả về một đối tượng iterator xác định phương thức __next__() truy cập lần lượt các phần tử trong vùng chứa. Khi không còn phần tử nào nữa, __next__() sẽ đưa ra một ngoại lệ StopIteration để báo cho vòng lặp for kết thúc. Bạn có thể gọi phương thức __next__() bằng hàm tích hợp next(); ví dụ này cho thấy tất cả hoạt động như thế nào:

>>> s = 'abc'
>>> it = iter(s)
>>> 
<đối tượng str_iterator tại 0x10c90e650>
>>> tiếp theo()
'a'
>>> tiếp theo()
'b'
>>> tiếp theo()
'c'
>>> tiếp theo()
Traceback (cuộc gọi gần đây nhất):
  Tệp "<stdin>", dòng 1, trong <module>
    tiếp theo(nó)
Dừng lại

Sau khi thấy các cơ chế đằng sau giao thức iterator, bạn có thể dễ dàng thêm hành vi của iterator vào các lớp của mình. Xác định phương thức __iter__() trả về một đối tượng bằng phương thức __next__(). Nếu lớp định nghĩa __next__() thì __iter__() chỉ có thể trả về self:

Lớp đảo ngược:
    """Trình vòng lặp để lặp lại một chuỗi ngược."""
    def __init__(bản thân, dữ liệu):
        self.data = dữ liệu
        self.index = len(dữ liệu)

    chắc chắn __iter__(tự):
        tự trở về

    chắc chắn __next__(tự):
        nếu self.index == 0:
            tăng StopIteration
        self.index = self.index - 1
        trả về self.data[self.index]
>>> rev = Đảo ngược('spam')
>>> iter(rev)
<__main__.Đối tượng đảo ngược tại 0x00A1DB50>
>>> cho char trong rev:
... in(char)
...
tôi
một
p
s

9.9. Máy phát điện

Generators là một công cụ đơn giản và mạnh mẽ để tạo các trình vòng lặp. Chúng được viết giống như các hàm thông thường nhưng sử dụng câu lệnh yield bất cứ khi nào chúng muốn trả về dữ liệu. Mỗi lần next() được gọi, trình tạo sẽ tiếp tục ở nơi nó đã dừng lại (nó ghi nhớ tất cả các giá trị dữ liệu và câu lệnh nào được thực thi lần cuối). Một ví dụ cho thấy rằng các trình tạo có thể dễ dàng tạo ra

def đảo ngược (dữ liệu):
    cho chỉ mục trong phạm vi(len(data)-1, -1, -1):
        dữ liệu năng suất [chỉ mục]
>>> cho char ngược lại('golf'):
... in(char)
...
f
tôi

g

Bất kỳ điều gì có thể được thực hiện bằng trình tạo cũng có thể được thực hiện bằng các trình vòng lặp dựa trên lớp như được mô tả trong phần trước. Điều làm cho máy phát điện trở nên nhỏ gọn là các phương thức __iter__()__next__() được tạo tự động.

Một tính năng quan trọng khác là các biến cục bộ và trạng thái thực thi được lưu tự động giữa các lệnh gọi. Điều này làm cho hàm dễ viết hơn và rõ ràng hơn nhiều so với cách tiếp cận sử dụng các biến thể hiện như self.indexself.data.

Ngoài việc tự động tạo phương thức và lưu trạng thái chương trình, khi các trình tạo kết thúc, chúng sẽ tự động tăng StopIteration. Kết hợp lại, các tính năng này giúp bạn dễ dàng tạo các trình vòng lặp mà không tốn nhiều công sức hơn việc viết một hàm thông thường.

9.10. Biểu thức tạo

Một số trình tạo đơn giản có thể được mã hóa ngắn gọn dưới dạng biểu thức sử dụng cú pháp tương tự như hiểu danh sách nhưng có dấu ngoặc đơn thay vì dấu ngoặc vuông. Các biểu thức này được thiết kế cho các tình huống trong đó trình tạo được sử dụng ngay lập tức bởi hàm kèm theo. Các biểu thức của trình tạo nhỏ gọn hơn nhưng kém linh hoạt hơn so với các định nghĩa của trình tạo đầy đủ và có xu hướng thân thiện với bộ nhớ hơn so với khả năng hiểu danh sách tương đương.

Ví dụ:

>>> sum(i*i for i in range(10)) # sum của hình vuông
285

>>> xvec = [10, 20, 30]
>>> yvec = [7, 5, 3]
>>> sum(x*y for x,y in zip(xvec, yvec)) tích # dot
260

>>> Unique_words = set(word for line in page for word in line.split())

>>> thủ khoa = max((student.gpa, sinh viên.name) dành cho sinh viên đã tốt nghiệp)

>>> dữ liệu = 'sân gôn'
>>> list(data[i] for i in range(len(data)-1, -1, -1))
['f', 'l', 'o', 'g']

Chú thích cuối trang