Рекомендації щодо анотацій

автор:

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), use o.__globals__ as the globals when calling eval().

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