Python's Self Type: Cách chú thích các phương thức trả về self
26/06/2023 10:29
Nếu không sử dụng đúng các gợi ý và Self type , việc khám phá các loại biến có thể trở thành một nhiệm vụ tẻ nhạt và tốn thời gian., Tìm hiểu về Python's Self Type ngay trong bài viết dưới đây!
Bạn đã bao giờ thấy mình bị lạc trong một kho mã Python khổng lồ, cố gắng theo dõi các loại biến dự định chưa? Nếu không sử dụng đúng các gợi ý và Self type , việc khám phá các loại biến có thể trở thành một nhiệm vụ tẻ nhạt và tốn thời gian. Có lẽ bạn là người thích sử dụng gợi ý kiểu nhưng không chắc cách chú thích các phương thức trả về hoặc các phiên bản khác của chính lớp đó. Đó là vấn đề mà bạn sẽ giải quyết trong hướng dẫn này.self
Tuy nhiên, trước tiên, bạn sẽ cần hiểu loại gợi ý là gì và chúng hoạt động như thế nào. Gợi ý kiểu cho phép bạn chỉ rõ một cách rõ ràng kiểu biến, đối số hàm và giá trị trả về. Điều này có thể làm cho mã của bạn dễ đọc và dễ bảo trì hơn, đặc biệt là khi mã phát triển về kích thước và độ phức tạp.
Bạn chỉ định các loại đối số của biến và hàm bằng dấu hai chấm ( :
) sau đó là loại dữ liệu , trong khi chú thích giá trị trả về sử dụng dấu gạch ngang–ký hiệu lớn hơn ( ->
) sau đó là loại trả về . Để xem một ví dụ, bạn có thể viết một hàm chấp nhận đầu vào là số lượng bánh mà bạn đã mua và giá mỗi chiếc bánh, sau đó xuất ra một chuỗi tóm tắt giao dịch của bạn:
>>> def buy_pies(num_pies: int, price_per_pie: float) -> str:
... total_cost = num_pies * price_per_pie
... return f"Yum! You spent ${total_cost} dollars on {num_pies} pies!"
...
Trong buy_pies()
, bạn nhập num_pies
biến với int
và price_per_pie
với float
. Bạn chú thích giá trị trả về với loại str
vì nó trả về một chuỗi.
Lưu ý: Có thể nhập gợi ý total_cost
biến cục bộ là total_cost: float
. Mặc dù điều này có vẻ là một ý tưởng hay, nhưng trình kiểm tra loại có thể tự động suy ra loại total_cost
from num_pies
và price_per_pie
, khiến chú thích không cần thiết cho total_cost
.
Các kiểu và chú thích trong Python thường không ảnh hưởng đến chức năng của mã nhưng nhiều trình kiểm tra kiểu tĩnh và IDE nhận ra chúng. Chẳng hạn, nếu bạn di chuột qua buy_pies()
trong Mã VS , thì bạn có thể thấy loại của từng đối số hoặc giá trị trả về:
Bạn cũng có thể sử dụng các chú thích khi làm việc với các lớp. Điều này có thể giúp các nhà phát triển khác hiểu được kiểu trả về mong đợi từ một phương thức, điều này có thể đặc biệt hữu ích khi làm việc với hệ thống phân cấp lớp phức tạp. Bạn thậm chí có thể chú thích các phương thức trả về một thể hiện của lớp.
Một trường hợp sử dụng cho các loại và chú thích là chú thích các phương thức trả về một thể hiện của lớp của chúng. Điều này đặc biệt hữu ích cho các phương thức của lớp và có thể ngăn ngừa sự nhầm lẫn phát sinh khi làm việc với kế thừa và xâu chuỗi phương thức. Thật không may, chú thích các phương pháp này có thể gây nhầm lẫn và gây ra các lỗi không mong muốn. Một cách tự nhiên để chú thích một phương thức như vậy là sử dụng tên lớp, nhưng cách sau sẽ không hoạt động:
# incorrect_self_type.py
from typing import Any
class Queue:
def __init__(self):
self.items: list[Any] = []
def enqueue(self, item: Any) -> Queue:
self.items.append(item)
return self
Trong ví dụ trên, .enqueue()
from Queue
nối thêm một mục vào hàng đợi và trả về thể hiện của lớp. .enqueue()
Mặc dù việc chú thích bằng tên lớp có vẻ trực quan , nhưng điều này gây ra cả lỗi kiểm tra kiểu tĩnh và thời gian chạy. Hầu hết các trình kiểm tra loại tĩnh đều nhận ra rằng mã đó Queue
không được xác định trước khi được sử dụng và nếu bạn cố chạy mã, thì bạn sẽ nhận được kết quả như sau NameError
:
Traceback (most recent call last):
...
NameError: name 'Queue' is not defined
Cũng sẽ có vấn đề khi kế thừa từ Queue
. Đặc biệt, một phương thức like .enqueue()
sẽ trả về Queue
ngay cả khi bạn gọi nó trên một lớp con của Queue
. Kiểu của Python Self
có thể xử lý các tình huống này, cung cấp một chú thích nhỏ gọn và dễ đọc, quan tâm đến sự tinh tế của các phương thức chú thích trả về một thể hiện của lớp kèm theo.
Trong hướng dẫn này, bạn sẽ khám phá Self
loại này một cách chi tiết và tìm hiểu cách sử dụng nó để viết mã dễ đọc và dễ bảo trì hơn. Cụ thể, bạn sẽ thấy cách chú thích một phương thức với kiểu Self
và đảm bảo rằng IDE của bạn sẽ nhận ra điều này. Bạn cũng sẽ kiểm tra các chiến lược thay thế cho các phương thức chú thích trả về một thể hiện của lớp và khám phá lý do tại sao Self
loại này được ưu tiên.
Cách chú thích một phương thức với Self Type trong Python
Do cú pháp ngắn gọn và trực quan của nó, như được định nghĩa bởi PEP 673 , Self
loại này là chú thích ưa thích cho các phương thức trả về một thể hiện của lớp của chúng. Loại này Self
có thể được nhập trực tiếp từ typing
mô-đun của Python trong phiên bản 3.11 trở lên. Đối với các phiên bản Python nhỏ hơn 3.11, Self
loại này có sẵn ở định dạng typing_extensions
.
Ví dụ, bạn sẽ chú thích cấu trúc dữ liệu ngăn xếp . Đặc biệt chú ý đến .push()
chú thích của bạn:
1# stack.py
2
3from typing import Any, Self
4
5class Stack:
6 def __init__(self) -> None:
7 self.items: list[Any] = []
8
9 def push(self, item: Any) -> Self:
10 self.items.append(item)
11 return self
12
13 def pop(self) -> Any:
14 if self.__bool__():
15 return self.items.pop()
16 else:
17 raise ValueError("Stack is empty")
18
19 def __bool__(self) -> bool:
20 return len(self.items) > 0
Bạn nhập Self
loại từ typing
trong dòng 3 và chú thích .push()
bằng -> Self
trong dòng 9. Điều này báo cho các trình kiểm tra loại tĩnh .push()
trả về một Stack
phiên bản, cho phép bạn tự tin xâu chuỗi nhiều lần đẩy lại với nhau. Lưu ý rằng thường không cần chú thích self
và cls
tham số, như đã thảo luận trong PEP 484 .
Lưu ý: Bạn có thể nhận thấy rằng bạn thực hiện .__bool__()
kiểm tra xem ngăn xếp có trống không. Phương thức này là một phần của mô hình dữ liệu của Python và được gọi là phương thức đặc biệt. Trong trường hợp này, việc xác định .__bool__()
cho phép bạn gọi bool()
hàm tích hợp từ bên trong hoặc bên ngoài lớp để kiểm tra xem ngăn xếp có trống không.
Việc đưa vào .__bool__()
cho phép sử dụng lớp trong các điều kiện Pythonic, chẳng hạn như if not stack: ...
, vì biểu thức trong một if
câu lệnh được đánh giá bằng cách sử dụng bool()
nội bộ. Điều này tạo thành cơ sở để xác định xem các giá trị được coi là đúng hay sai theo thuật ngữ Boolean .
Đối với các phiên bản Python nhỏ hơn 3.11, bạn có thể sử dụng typing_extensions
mô-đun để nhập Self
loại và bạn có thể giữ nguyên mã còn lại:
# stack.py
from typing import Any
from typing_extensions import Self
# ...
Bằng cách nhập Self
từ typing_extensions
, bạn có thể sử dụng Self
để chú thích các phương thức giống như cách bạn sẽ sử dụng typing
mô-đun trong Python 3.11.
Lưu ý: typing_extensions
là thư viện của bên thứ ba mà bạn cài đặt với pip
. Bởi vì typing
là một phần của thư viện tiêu chuẩn, nó chỉ có thể được cập nhật trong các bản phát hành Python thông thường, do đó typing_extensions
cần phải nhập các tính năng mới vào các phiên bản Python cũ.
Như bạn đã học, .push()
hãy thêm các mục vào ngăn xếp và trả về phiên bản ngăn xếp đã cập nhật, yêu cầu Self
chú thích. Điều này cho phép bạn xâu chuỗi tuần tự .push()
các phương thức vào một phiên bản ngăn xếp, làm cho mã của bạn ngắn gọn và dễ đọc hơn:
>>> from stack import Stack
>>> stack = Stack()
>>> stack.push(1).push(2).push(3).pop()
3
>>> stack.items
[1, 2]
Trong ví dụ trên, bạn khởi tạo một Stack
thể hiện, đẩy ba phần tử vào ngăn xếp theo trình tự và bật một phần tử. Bằng cách đưa vào Self
làm chú thích, bạn có thể kiểm tra .push()
trực tiếp từ một đối tượng được khởi tạo để xem nó trả về cái gì:
Khi bạn di chuột qua .push()
Mã VS, bạn có thể thấy loại trả về là Stack
, như ghi chú chú thích. Với chú thích này, những người khác đọc mã của bạn sẽ không phải xem Stack
định nghĩa để biết nó .push()
trả về thể hiện của lớp.
Tiếp theo, bạn sẽ xem xét một lớp đại diện cho trạng thái và logic của tài khoản ngân hàng. Lớp BankAccount
hỗ trợ một số hành động, chẳng hạn như gửi và rút tiền, cập nhật trạng thái của tài khoản và trả về thể hiện của lớp. Ví dụ: .deposit()
lấy một số tiền làm đầu vào, tăng số dư nội bộ của tài khoản và trả về phiên bản để bạn có thể xâu chuỗi các phương thức khác:
# accounts.py
from dataclasses import dataclass
from typing import Self
@dataclass
class BankAccount:
account_number: int
balance: float
def display_balance(self) -> Self:
print(f"Account Number: {self.account_number}")
print(f"Balance: ${self.balance:,.2f}\n")
return self
def deposit(self, amount: float) -> Self:
self.balance += amount
return self
def withdraw(self, amount: float) -> Self:
if self.balance >= amount:
self.balance -= amount
else:
print("Insufficient balance")
return self
Trong BankAccount
, bạn chú thích .display_balance()
, .deposit()
, và .withdraw()
with Self
để trả về một thể hiện của lớp. Bạn có thể khởi tạo BankAccount
và gửi hoặc rút tiền với số lần tùy ý:
>>> from accounts import BankAccount
>>> account = BankAccount(account_number=1534899324, balance=50)
>>> (
... account.display_balance()
... .deposit(50)
... .display_balance()
... .withdraw(30)
... .display_balance()
... )
Account Number: 1534899324
Balance: $50.00
Account Number: 1534899324
Balance: $100.00
Account Number: 1534899324
Balance: $70.00
BankAccount(account_number=1534899324, balance=70)
Tại đây, bạn xác định một BankAccount
phiên bản có số tài khoản và số dư ban đầu. Sau đó, bạn xâu chuỗi nhiều phương thức thực hiện gửi tiền, rút tiền và hiển thị số dư, mỗi phương thức trả về self
. REPL tự động in giá trị trả về của biểu thức cuối cùng trong chuỗi phương thức, .display_balance()
. Đầu ra của nó, BankAccount(account_number=1534899324, balance=50)
, cung cấp một biểu diễn đẹp của lớp.
Lưu ý: Bạn có thể nhận thấy đó BankAccount
là một lớp dữ liệu . Các lớp dữ liệu là một cách tuyệt vời để xác định các lớp và chúng được trang bị nhiều tính năng hữu ích. Bởi vì BankAccount
là một lớp dữ liệu, bạn không cần định nghĩa hàm tạo và lớp này có một biểu diễn chuỗi đẹp mắt từ phương .__repr__()
thức mặc định .
Các trường hợp sử dụng khác cho Self
loại này là các phương thức lớp và phân cấp thừa kế . Chẳng hạn, nếu lớp cha và lớp con của nó có các phương thức trả về self
, thì bạn có thể chú thích cả hai bằng Self
kiểu.
Thật thú vị, khi một đối tượng con gọi một phương thức cha mà trả về self
, bộ kiểm tra kiểu sẽ chỉ ra rằng phương thức đó trả về một thể hiện của lớp con. Bạn có thể thấy ý tưởng này bằng cách tạo một SavingsAccount
lớp kế thừa từ BankAccount
:
# accounts.py
import random
from dataclasses import dataclass
from typing import Self
# ...
@dataclass
class SavingsAccount(BankAccount):
interest_rate: float
@classmethod
def from_application(
cls, deposit: float = 0, interest_rate: float = 1
) -> Self:
# Generate a random seven-digit bank account number
account_number = random.randint(1000000, 9999999)
return cls(account_number, deposit, interest_rate)
def calculate_interest(self) -> float:
return self.balance * self.interest_rate / 100
def add_interest(self) -> Self:
self.deposit(self.calculate_interest())
return self
SavingsAccount
có một .from_application()
phương thức tạo một thể hiện của lớp từ các tham số của ứng viên thay vì thông qua hàm tạo thông thường . Nó cũng có .add_interest()
, gửi tiền lãi vào số dư tài khoản và trả về thể hiện của lớp. Bạn có thể tăng khả năng đọc và khả năng bảo trì của cả hai phương pháp này với loại Self
.
Tiếp theo, tạo một SavingsAccount
đối tượng từ một ứng dụng mới, thực hiện gửi tiền và rút tiền, đồng thời thêm lãi suất:
>>> from accounts import SavingsAccount
>>> savings = SavingsAccount.from_application(deposit=100, interest_rate=5)
>>> (
... savings.display_balance()
... .add_interest()
... .display_balance()
... .deposit(50)
... .display_balance()
... .withdraw(30)
... .add_interest()
... .display_balance()
... )
Account Number: 3631051
Balance: $100.00
Account Number: 3631051
Balance: $105.00
Account Number: 3631051
Balance: $155.00
Account Number: 3631051
Balance: $131.25
SavingsAccount(account_number=3631051, balance=131.25, interest_rate=5)