Рекомендації щодо анотацій¶
- автор:
Larry Hastings
Доступ до анотацій Dict об’єкта в Python 3.10 і новіших версіях¶
Python 3.10 додає нову функцію до стандартної бібліотеки: inspect.get_annotations()
. У версіях Python 3.10 і новіших, виклик цієї функції є найкращою практикою для доступу до dict анотацій будь-якого об’єкта, який підтримує анотації. Ця функція також може «відмінювати» рядкові анотації для вас.
Якщо з якоїсь причини inspect.get_annotations()
непридатний для вашого випадку використання, ви можете отримати доступ до елемента даних __annotations__
вручну. У Python 3.10 також змінилася найкраща практика щодо цього: починаючи з Python 3.10, o.__annotations__
гарантовано завжди працюватиме з функціями, класами та модулями Python. Якщо ви впевнені, що об’єкт, який ви досліджуєте, є одним із цих трьох специфічних об’єктів, ви можете просто використати o.__annotations__
, щоб отримати dict анотацій об’єкта.
Однак інші типи викликів, наприклад, виклики, створені functools.partial()
, можуть не мати визначеного атрибута __annotations__
. Під час доступу до __annotations__
можливо невідомого об’єкта, найкраща практика в Python версії 3.10 і новіших — викликати getattr()
з трьома аргументами, наприклад getattr(o, '__annotations__', None)
.
Before Python 3.10, accessing __annotations__
on a class that
defines no annotations but that has a parent class with
annotations would return the parent’s __annotations__
.
In Python 3.10 and newer, the child class’s annotations
will be an empty dict instead.
Доступ до анотацій Dict об’єкта в Python 3.9 і старіших версіях¶
У Python 3.9 і старіших версіях доступ до анотацій dict об’єкта набагато складніший, ніж у новіших версіях. Проблема полягає в недоліку дизайну в цих старих версіях Python, зокрема щодо анотацій класів.
Найкраща практика доступу до анотацій dict інших об’єктів — функцій, інших викликів і модулів — така ж, як і найкраща практика для 3.10, припускаючи, що ви не викликаєте inspect.get_annotations()
: вам слід використовувати три- аргумент getattr()
для доступу до атрибута __annotations__
об’єкта.
На жаль, це не найкраща практика для занять. Проблема полягає в тому, що, оскільки __annotations__
є необов’язковим для класів, і оскільки класи можуть успадковувати атрибути від своїх базових класів, доступ до атрибута __annotations__
класу може ненавмисно повернути анотації dict base class. Як приклад:
class Base:
a: int = 3
b: str = 'abc'
class Derived(Base):
pass
print(Derived.__annotations__)
Це надрукує анотації dict з Base
, а не Derived
.
Your code will have to have a separate code path if the object
you’re examining is a class (isinstance(o, type)
).
In that case, best practice relies on an implementation detail
of Python 3.9 and before: if a class has annotations defined,
they are stored in the class’s __dict__
dictionary. Since
the class may or may not have annotations defined, best practice
is to call the get()
method on the class dict.
Ось приклад коду, який безпечно отримує доступ до атрибута __annotations__
довільного об’єкта в Python 3.9 і раніше:
if isinstance(o, type):
ann = o.__dict__.get('__annotations__', None)
else:
ann = getattr(o, '__annotations__', None)
Після виконання цього коду ann
має бути або словником, або None
. Перед подальшим вивченням радимо ще раз перевірити тип ann
за допомогою isinstance()
.
Note that some exotic or malformed type objects may not have
a __dict__
attribute, so for extra safety you may also wish
to use getattr()
to access __dict__
.
Ручне видалення струнних анотацій¶
У ситуаціях, коли деякі анотації можуть бути «рядковими», і ви бажаєте оцінити ці рядки для створення значень Python, які вони представляють, дійсно найкраще викликати inspect.get_annotations()
, щоб зробити цю роботу за вас.
Якщо ви використовуєте Python 3.9 або старішу версію, або якщо з якоїсь причини ви не можете використовувати inspect.get_annotations()
, вам потрібно буде скопіювати його логіку. Радимо перевірити реалізацію inspect.get_annotations()
у поточній версії Python і застосувати подібний підхід.
У двох словах, якщо ви бажаєте оцінити рядкову анотацію довільного об’єкта o
:
Якщо
o
є модулем, використовуйтеo.__dict__
якglobals
під час викликуeval()
.Якщо
o
є класом, використовуйтеsys.modules[o.__module__].__dict__
якglobals
, аdict(vars(o))
якlocals
, під час викликуeval()
.Якщо
o
є обернутим викликом за допомогоюfunctools.update_wrapper()
,functools.wraps()
абоfunctools.partial()
, ітеративно розгортайте його, звертаючись доo.__wrapped__
абоo.func
відповідно, доки ви не знайдете кореневу розгорнуту функцію.If
o
is a callable (but not a class), useo.__globals__
as the globals when callingeval()
.
Однак не всі рядкові значення, які використовуються як анотації, можна успішно перетворити на значення Python за допомогою eval()
. Рядкові значення теоретично можуть містити будь-який дійсний рядок, і на практиці є дійсні випадки використання підказок типу, які вимагають анотування рядковими значеннями, які конкретно неможливо оцінити. Наприклад:
PEP 604 типи об’єднання з використанням
|
, перш ніж підтримку цього було додано в Python 3.10.Визначення, які не потрібні під час виконання, імпортуються лише тоді, коли
typing.TYPE_CHECKING
має значення true.
Якщо eval()
спробує обчислити такі значення, це зазнає невдачі та викличе виняток. Таким чином, під час розробки API бібліотеки, яка працює з анотаціями, рекомендується намагатися оцінити значення рядка лише тоді, коли це явно вимагається від викликаючого.
Найкращі методи роботи з __annotations__
в будь-якій версії Python¶
Вам слід уникати безпосереднього призначення члену
__annotations__
об’єктів. Дозвольте Python керувати налаштуванням__annotations__
.Якщо ви призначаєте безпосередньо члену
__annotations__
об’єкта, ви завжди повинні встановлювати його на об’єктdict
.Якщо ви здійснюєте прямий доступ до елемента
__annotations__
об’єкта, ви повинні переконатися, що це словник, перш ніж намагатися перевірити його вміст.Слід уникати змінення
__annotations__
dicts.Слід уникати видалення атрибута
__annotations__
об’єкта.
__annotations__
Примхи¶
У всіх версіях Python 3 функціональні об’єкти відкладено створюють анотації dict, якщо для цього об’єкта не визначено анотацій. Ви можете видалити атрибут __annotations__
за допомогою del fn.__annotations__
, але якщо ви потім отримаєте доступ до fn.__annotations__
, об’єкт створить новий порожній dict, який він зберігатиме та повертатиме як свої анотації. Видалення анотацій у функції до того, як вона ліниво створить свої анотації dict, викличе AttributeError
; використання del fn.__annotations__
двічі поспіль гарантовано завжди викидає AttributeError
.
Усе, що наведено вище, також стосується об’єктів класу та модуля в Python 3.10 і новіших версіях.
У всіх версіях Python 3 для __annotations__
для об’єкта функції можна встановити значення None
. Однак подальший доступ до анотацій цього об’єкта за допомогою fn.__annotations__
призведе до відкладеного створення порожнього словника згідно з першим параграфом цього розділу. Це не стосується модулів і класів у будь-якій версії Python; ці об’єкти дозволяють установлювати __annotations__
на будь-яке значення Python і збережуть будь-яке встановлене значення.
Якщо Python створить для вас рядкові анотації (за допомогою from __future__ import annotations
), і ви вкажете рядок як анотацію, сам рядок буде взято в лапки. По суті, анотація взята в лапки двічі. Наприклад:
from __future__ import annotations
def foo(a: "str"): pass
print(foo.__annotations__)
Це друкує {'a': "'str'"}
. Це насправді не слід вважати «химерністю»; це згадується тут просто тому, що це може бути несподіваним.