Документация Python неофициальный перевод
Содержание страницы

Лучшие практики работы с аннотациямиAnnotations Best Practices

автор:

Larry Hastings

Доступ к словарю аннотаций объекта в Python 3.10 и новееAccessing The Annotations Dict Of An Object In Python 3.10 And Newer

Python 3.10 добавляет в стандартную библиотеку новую функцию: inspect.get_annotations(). В версиях Python с 3.10 по 3.13 вызов этой функции является лучшей практикой для доступа к словарю аннотаций любого объекта, поддерживающего аннотации. Эта функция также может преобразовывать строковые аннотации обратно в исходный вид.

В Python 3.14 появился новый модуль annotationlib с функциональностью для работы с аннотациями. Он включает функцию annotationlib.get_annotations(), которая заменяет inspect.get_annotations().

Если по какой-то причине inspect.get_annotations() не подходит для вашего случая, вы можете получить доступ к элементу данных __annotations__ вручную. Лучшая практика для этого также изменилась в Python 3.10: начиная с Python 3.10, o.__annotations__ гарантированно всегда работает с функциями, классами и модулями Python. Если вы уверены, что исследуемый объект является одним из этих трёх конкретных объектов, вы можете просто использовать o.__annotations__ для получения словаря аннотаций объекта.

Однако другие типы вызываемых объектов – например, созданные с помощью functools.partial() – могут не иметь атрибута __annotations__. При доступе к __annotations__ возможно неизвестного объекта лучшая практика в Python версиях 3.10 и новее – вызывать getattr() с тремя аргументами, например getattr(o, '__annotations__', None).

До Python 3.10 доступ к __annotations__ у класса, который не определяет аннотации, но имеет родительский класс с аннотациями, возвращал __annotations__ родителя. В Python 3.10 и новее аннотации дочернего класса вместо этого будут пустым словарём.

Доступ к словарю аннотаций объекта в Python 3.9 и старшеAccessing The Annotations Dict Of An Object In Python 3.9 And Older

В Python 3.9 и старше доступ к словарю аннотаций объекта намного сложнее, чем в более новых версиях. Проблема заключается в ошибке проектирования в этих старых версиях Python, связанной с аннотациями классов.

Лучшая практика для доступа к словарю аннотаций других объектов – функций, других вызываемых объектов и модулей – такая же, как для 3.10, при условии, что вы не вызываете inspect.get_annotations(): следует использовать getattr() с тремя аргументами для доступа к атрибуту __annotations__ объекта.

К сожалению, это не лучшая практика для классов. Проблема в том, что, поскольку __annotations__ является необязательным для классов и классы могут наследовать атрибуты от базовых классов, доступ к атрибуту __annotations__ класса может непреднамеренно вернуть словарь аннотаций базового класса. Например:

class Base:
    a: int = 3
    b: str = 'abc'

class Derived(Base):
    pass

print(Derived.__annotations__)

Это выведет словарь аннотаций из Base, а не из Derived.

Вашему коду потребуется отдельный путь обработки, если исследуемый объект является классом (isinstance(o, type)). В этом случае лучшая практика опирается на деталь реализации Python 3.9 и более ранних версий: если у класса определены аннотации, они хранятся в словаре __dict__ класса. Поскольку аннотации могут быть определены, а могут и не быть, лучшей практикой является вызов метода get() на словаре класса.

Собирая всё вместе, вот пример кода, который безопасно обращается к атрибуту __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__.

Ручное обратное преобразование строковых аннотацийManually Un-Stringizing Stringized Annotations

В ситуациях, когда некоторые аннотации могут быть «строкизированы» и требуется вычислить эти строки, чтобы получить представляемые ими значения 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 по необходимости, пока не будет найдена корневая необёрнутая функция.

  • Если o – вызываемый объект (но не класс), o.__globals__ следует передать как globals при вызове eval().

Однако не все строковые значения, используемые в качестве аннотаций, могут быть успешно преобразованы в значения Python с помощью eval(). Строковые значения теоретически могут содержать любую допустимую строку, и на практике существуют обоснованные сценарии использования подсказок типов, требующие аннотирования строковыми значениями, которые не могут быть вычислены. Например:

Если eval() попытается вычислить такие значения, произойдёт ошибка и будет возбуждено исключение. Поэтому при разработке библиотечного API, работающего с аннотациями, рекомендуется пытаться вычислять строковые значения только по явному запросу вызывающей стороны.

Рекомендации по __annotations__ в любой версии PythonBest Practices For __annotations__ In Any Python Version

  • Следует избегать прямого присваивания атрибуту __annotations__ объектов. Управление установкой __annotations__ следует оставить Python.

  • Если всё же выполняется прямое присваивание атрибуту __annotations__ объекта, всегда следует устанавливать его равным объекту dict.

  • Следует избегать прямого доступа к __annotations__ у любого объекта. Вместо этого следует использовать annotationlib.get_annotations() (Python 3.14+) или inspect.get_annotations() (Python 3.10+).

  • Если всё же выполняется прямой доступ к атрибуту __annotations__ объекта, следует убедиться, что это словарь, прежде чем пытаться исследовать его содержимое.

  • Следует избегать изменения словарей __annotations__.

  • Следует избегать удаления атрибута __annotations__ объекта.

__annotations__ Особенности__annotations__ Quirks

Во всех версиях Python 3 объекты-функции лениво создают словарь аннотаций, если для этого объекта аннотации не определены. Можно удалить атрибут __annotations__ с помощью del fn.__annotations__, но если затем обратиться к fn.__annotations__, объект создаст новый пустой словарь, который будет сохранён и возвращён как его аннотации. Удаление аннотаций у функции до того, как она лениво создала свой словарь аннотаций, вызовет исключение 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'"}. Это не стоит считать «странностью»; это упоминается здесь просто потому, что может показаться неожиданным.

Если используется класс с пользовательским метаклассом и выполняется доступ к __annotations__ у этого класса, может наблюдаться неожиданное поведение; см. 749 для примеров. Этих особенностей можно избежать, используя annotationlib.get_annotations() в Python 3.14+ или inspect.get_annotations() в Python 3.10+. В более старых версиях Python этих ошибок можно избежать, обращаясь к аннотациям через __dict__ класса (например, cls.__dict__.get('__annotations__', None)).

В некоторых версиях Python экземпляры классов могут иметь атрибут __annotations__. Однако это не поддерживаемая функциональность. Если нужны аннотации экземпляра, можно использовать type() для доступа к его классу (например, annotationlib.get_annotations(type(myinstance)) в Python 3.14+).