Рекомендації щодо анотацій¶
- автор:
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
ois 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'"}. Це насправді не слід вважати «химерністю»; це згадується тут просто тому, що це може бути несподіваним.