Рекомендації щодо анотацій¶
- автор
Larry Hastings
Анотація
Цей документ розроблено, щоб узагальнити найкращі методи роботи з анотаціями dicts. Якщо ви пишете код на Python, який перевіряє __annotations__ на об’єктах Python, радимо дотримуватися вказівок, описаних нижче.
Документ складається з чотирьох розділів: найкращі методи доступу до анотацій об’єкта в Python версії 3.10 і новіших, найкращі методи доступу до анотацій об’єктів у Python 3.9 і старіших версій, інші найкращі практики для __annotations__, які застосовуються до будь-якої версії Python, а особливості __annotations__.
Зверніть увагу, що в цьому документі йдеться саме про роботу з __annotations__, а не про використання анотацій for. Якщо ви шукаєте інформацію про те, як використовувати «підказки типу» у вашому коді, перегляньте модуль typing.
Доступ до анотацій 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 thegetmethod 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 usegetattr()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'"}. Це насправді не слід вважати «химерністю»; це згадується тут просто тому, що це може бути несподіваним.