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

автор:

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