Các phương pháp hay nhất về chú thích

tác giả:

Larry Hastings

Truy cập chú thích của một đối tượng trong Python 3.10 trở lên

Python 3.10 thêm một chức năng mới vào thư viện chuẩn: inspect.get_annotations(). Trong các phiên bản Python 3.10 đến 3.13, việc gọi hàm này là cách tốt nhất để truy cập vào lệnh chú thích của bất kỳ đối tượng nào hỗ trợ chú thích. Chức năng này cũng có thể "hủy xâu chuỗi" các chú thích dạng chuỗi cho bạn.

Trong Python 3.14, có một mô-đun annotationlib mới có chức năng làm việc với các chú thích. Điều này bao gồm chức năng annotationlib.get_annotations(), thay thế inspect.get_annotations().

Nếu vì lý do nào đó, inspect.get_annotations() không phù hợp với trường hợp sử dụng của bạn, bạn có thể truy cập thành viên dữ liệu __annotations__ theo cách thủ công. Cách thực hành tốt nhất cho điều này cũng đã thay đổi trong Python 3.10: kể từ Python 3.10, o.__annotations__ được đảm bảo always hoạt động trên các hàm, lớp và mô-đun Python. Nếu bạn chắc chắn đối tượng bạn đang kiểm tra là một trong ba đối tượng specific này, bạn có thể chỉ cần sử dụng o.__annotations__ để lấy lệnh chú thích của đối tượng.

Tuy nhiên, các loại lệnh gọi khác--ví dụ: các lệnh gọi được tạo bởi functools.partial()--có thể không được xác định thuộc tính __annotations__. Khi truy cập __annotations__ của một đối tượng có thể không xác định, cách tốt nhất trong phiên bản Python 3.10 trở lên là gọi getattr() với ba đối số, ví dụ: getattr(o, '__annotations__', None).

Trước Python 3.10, việc truy cập __annotations__ trên một lớp không xác định chú thích nhưng có lớp cha có chú thích sẽ trả về __annotations__ của cha. Trong Python 3.10 trở lên, các chú thích của lớp con sẽ là một lệnh trống.

Truy cập chú thích của một đối tượng trong Python 3.9 trở lên

Trong Python 3.9 trở lên, việc truy cập vào chú thích dict của một đối tượng phức tạp hơn nhiều so với các phiên bản mới hơn. Vấn đề là một lỗi thiết kế trong các phiên bản Python cũ hơn này, đặc biệt liên quan đến các chú thích lớp.

Cách tốt nhất để truy cập vào chú thích chính tả của các đối tượng khác--hàm, các lệnh gọi khác và mô-đun--cũng giống như cách thực hành tốt nhất cho 3.10, giả sử bạn không gọi inspect.get_annotations(): bạn nên sử dụng getattr() ba đối số để truy cập thuộc tính __annotations__ của đối tượng.

Thật không may, đây không phải là cách thực hành tốt nhất cho lớp học. Vấn đề là ở chỗ, vì __annotations__ là tùy chọn trên các lớp và vì các lớp có thể kế thừa các thuộc tính từ các lớp cơ sở của chúng, nên việc truy cập thuộc tính __annotations__ của một lớp có thể vô tình trả về lệnh chú thích của base class.. Ví dụ:

lớp  sở:
    một: int = 3
    b: str = 'abc'

lớp phái sinh ( sở):
    vượt qua

print(Derived.__annotations__)

Điều này sẽ in các chú thích từ Base, không phải Derived.

Mã của bạn sẽ phải có đường dẫn mã riêng nếu đối tượng bạn đang kiểm tra là một lớp (isinstance(o, type)). Trong trường hợp đó, cách thực hành tốt nhất sẽ dựa vào chi tiết triển khai của Python 3.9 trở về trước: nếu một lớp có các chú thích được xác định thì chúng sẽ được lưu trữ trong từ điển __dict__ của lớp. Vì lớp có thể có hoặc không có chú thích được xác định, cách tốt nhất là gọi phương thức get() trên lệnh lớp.

Tóm lại, đây là một số mã mẫu truy cập an toàn thuộc tính __annotations__ trên một đối tượng tùy ý trong Python 3.9 trở về trước

nếu isinstance(o, type):
    ann = o.__dict__.get('__annotations__', None)
khác:
    ann = getattr(o, '__annotations__', None)

Sau khi chạy mã này, ann phải là từ điển hoặc None. Bạn nên kiểm tra kỹ loại ann bằng isinstance() trước khi kiểm tra thêm.

Lưu ý rằng một số đối tượng loại kỳ lạ hoặc không đúng định dạng có thể không có thuộc tính __dict__, vì vậy để an toàn hơn, bạn cũng có thể muốn sử dụng getattr() để truy cập __dict__.

Chú thích được xâu chuỗi theo cách thủ công

Trong trường hợp một số chú thích có thể được "xâu chuỗi" và bạn muốn đánh giá các chuỗi đó để tạo ra các giá trị Python mà chúng đại diện, thì tốt nhất bạn nên gọi inspect.get_annotations() để thực hiện công việc này cho bạn.

Nếu bạn đang sử dụng Python 3.9 trở lên hoặc nếu vì lý do nào đó bạn không thể sử dụng inspect.get_annotations(), bạn sẽ cần sao chép logic của nó. Bạn nên kiểm tra việc triển khai inspect.get_annotations() trong phiên bản Python hiện tại và làm theo cách tiếp cận tương tự.

Tóm lại, nếu bạn muốn đánh giá một chú thích được xâu chuỗi trên một đối tượng tùy ý o:

  • Nếu o là một mô-đun, hãy sử dụng o.__dict__ làm globals khi gọi eval().

  • Nếu o là một lớp, hãy sử dụng sys.modules[o.__module__].__dict__ làm globalsdict(vars(o)) làm locals khi gọi eval().

  • Nếu o là một hàm có thể gọi được gói bằng cách sử dụng functools.update_wrapper(), functools.wraps() hoặc functools.partial(), hãy mở gói lặp đi lặp lại bằng cách truy cập o.__wrapped__ hoặc o.func nếu thích hợp, cho đến khi bạn tìm thấy hàm được mở gói gốc.

  • Nếu o có thể gọi được (nhưng không phải là một lớp), hãy sử dụng o.__globals__ làm toàn cục khi gọi eval().

Tuy nhiên, không phải tất cả các giá trị chuỗi được sử dụng làm chú thích đều có thể được eval() chuyển thành công thành giá trị Python. Về mặt lý thuyết, các giá trị chuỗi có thể chứa bất kỳ chuỗi hợp lệ nào và trong thực tế, có những trường hợp sử dụng hợp lệ cho các gợi ý loại yêu cầu chú thích bằng các giá trị chuỗi được đánh giá cụ thể là can't. Ví dụ:

  • Các loại kết hợp PEP 604 sử dụng |, trước khi hỗ trợ cho việc này được thêm vào Python 3.10.

  • Các định nghĩa không cần thiết trong thời gian chạy, chỉ được nhập khi typing.TYPE_CHECKING là đúng.

Nếu eval() cố gắng đánh giá các giá trị như vậy, nó sẽ thất bại và đưa ra một ngoại lệ. Vì vậy, khi thiết kế thư viện API hoạt động với chú thích, bạn chỉ nên cố gắng đánh giá các giá trị chuỗi khi được người gọi yêu cầu rõ ràng.

Các phương pháp hay nhất cho __annotations__ trong mọi phiên bản Python

  • Bạn nên tránh gán trực tiếp cho thành viên __annotations__ của đối tượng. Hãy để Python quản lý cài đặt __annotations__.

  • Nếu bạn gán trực tiếp cho thành viên __annotations__ của một đối tượng, bạn phải luôn đặt nó thành đối tượng dict.

  • Bạn nên tránh truy cập trực tiếp __annotations__ trên bất kỳ đối tượng nào. Thay vào đó, hãy sử dụng annotationlib.get_annotations() (Python 3.14+) hoặc inspect.get_annotations() (Python 3.10+).

  • Nếu bạn truy cập trực tiếp vào thành viên __annotations__ của một đối tượng, bạn nên đảm bảo rằng đó là một từ điển trước khi thử kiểm tra nội dung của nó.

  • Bạn nên tránh sửa đổi các lệnh __annotations__.

  • Bạn nên tránh xóa thuộc tính __annotations__ của một đối tượng.

__annotations__ Quirks

Trong tất cả các phiên bản của Python 3, các đối tượng hàm tạo lười biếng một lệnh chú thích nếu không có chú thích nào được xác định trên đối tượng đó. Bạn có thể xóa thuộc tính __annotations__ bằng cách sử dụng del fn.__annotations__, nhưng nếu sau đó bạn truy cập fn.__annotations__, đối tượng sẽ tạo một lệnh trống mới mà nó sẽ lưu trữ và trả về dưới dạng chú thích. Việc xóa các chú thích trên một hàm trước khi nó được tạo ra một cách lười biếng lệnh chú thích của nó sẽ tạo ra một AttributeError; sử dụng del fn.__annotations__ hai lần liên tiếp đảm bảo sẽ luôn tạo ra AttributeError.

Mọi thứ trong đoạn trên cũng áp dụng cho các đối tượng lớp và mô-đun trong Python 3.10 trở lên.

Trong tất cả các phiên bản Python 3, bạn có thể đặt __annotations__ trên đối tượng hàm thành None. Tuy nhiên, sau đó việc truy cập các chú thích trên đối tượng đó bằng fn.__annotations__ sẽ tạo ra một từ điển trống theo đoạn đầu tiên của phần này. Điều này đúng với not đối với các mô-đun và lớp, trong bất kỳ phiên bản Python nào; những đối tượng đó cho phép đặt __annotations__ thành bất kỳ giá trị Python nào và sẽ giữ lại bất kỳ giá trị nào được đặt.

Nếu Python xâu chuỗi các chú thích cho bạn (sử dụng from __future__ import annotations) và bạn chỉ định một chuỗi làm chú thích thì chính chuỗi đó sẽ được trích dẫn. Trong thực tế, chú thích được trích dẫn twice. Ví dụ:

từ __future__ nhập chú thích
def foo(a: "str"): vượt qua

print(foo.__annotations__)

Điều này in {'a': "'str'"}. Điều này thực sự không nên được coi là một "điều kỳ quặc"; nó được đề cập ở đây đơn giản chỉ vì nó có thể gây ngạc nhiên.

Nếu bạn sử dụng một lớp có siêu dữ liệu tùy chỉnh và truy cập __annotations__ trên lớp đó, bạn có thể quan sát thấy hành vi không mong muốn; xem 749 để biết một số ví dụ. Bạn có thể tránh những điều kỳ quặc này bằng cách sử dụng annotationlib.get_annotations() trên Python 3.14+ hoặc inspect.get_annotations() trên Python 3.10+. Trên các phiên bản Python cũ hơn, bạn có thể tránh những lỗi này bằng cách truy cập các chú thích từ __dict__ của lớp (ví dụ: cls.__dict__.get('__annotations__', None)).

Trong một số phiên bản Python, phiên bản của các lớp có thể có thuộc tính __annotations__. Tuy nhiên, đây không phải là chức năng được hỗ trợ. Nếu cần chú thích của một phiên bản, bạn có thể sử dụng type() để truy cập lớp của nó (ví dụ: annotationlib.get_annotations(type(myinstance)) trên Python 3.14+).