Рекомендації щодо анотацій¶
- автор:
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
.
Ваш код повинен мати окремий кодовий шлях, якщо об’єкт, який ви перевіряєте, є класом (isinstance(o, type)
). У цьому випадку найкраща практика покладається на деталі реалізації Python 3.9 і раніше: якщо клас має визначені анотації, вони зберігаються в словнику __dict__
класу. Оскільки клас може мати або не мати визначених анотацій, найкращою практикою є виклик методу get
у 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()
.
Зауважте, що деякі об’єкти екзотичного чи неправильного типу можуть не мати атрибута __dict__
, тому для додаткової безпеки ви також можете використовувати getattr()
для доступу до __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'"}
. Це насправді не слід вважати «химерністю»; це згадується тут просто тому, що це може бути несподіваним.