Spec-Zone.ru › Python 3.13

Рекомендации по работе с аннотациями

автор:

Larry Hastings

Краткое описание

Этот документ содержит рекомендации по работе со словарями аннотаций. Если вы пишете код на Python, который анализирует __annotations__ объектов Python, рекомендуем следовать приведённым ниже рекомендациям.

Документ состоит из четырёх разделов: рекомендации по доступу к аннотациям объекта в Python 3.10 и новее, рекомендации по доступу к аннотациям объекта в Python 3.9 и старше, другие рекомендации по __annotations__, применимые к любой версии Python, и особенности __annotations__.

Обратите внимание: этот документ посвящён работе с __annotations__, а не их использованию. Если вам нужна информация о том, как использовать «подсказки типов» в коде, обратитесь к модулю typing.

Доступ к словарю аннотаций объекта в Python 3.10 и новее

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

Если по какой-либо причине 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 и старше

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

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

К сожалению, для классов этот способ не рекомендуется. Проблема в том, что __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__.

Преобразование строковых аннотаций вручную

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

Если вы используете Python 3.9 или старше либо по какой-либо причине не можете использовать inspect.get_annotations(), вам потребуется повторить её логику. Рекомендуется изучить реализацию inspect.get_annotations() в текущей версии Python и придерживаться аналогичного подхода.

Кратко: если вы хотите вычислить строковую аннотацию произвольного объекта o:

  • Если o — модуль, при вызове eval() используйте o.__dict__ в качестве globals.
  • Если o — класс, при вызове eval() используйте sys.modules[o.__module__].__dict__ в качестве globals, а dict(vars(o)) — в качестве locals.
  • Если o — обёрнутый вызываемый объект, созданный с помощью functools.update_wrapper(), functools.wraps() или functools.partial(), последовательно снимайте обёртки, обращаясь к o.__wrapped__ или o.func, в зависимости от ситуации, пока не найдёте исходную функцию без обёрток.
  • Если o — вызываемый объект (но не класс), при вызове eval() используйте o.__globals__ в качестве глобального пространства имён.

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

  • PEP 604 — объединённые типы с использованием | до появления поддержки этой возможности в Python 3.10.
  • Определения, которые не нужны во время выполнения и импортируются только при истинном значении typing.TYPE_CHECKING.

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

Рекомендации по работе с __annotations__ в любой версии Python

  • Не следует напрямую присваивать значения элементу __annotations__ объекта. Позвольте Python самостоятельно управлять установкой __annotations__.
  • Если вы напрямую присваиваете значение элементу __annotations__ объекта, всегда устанавливайте объект типа dict.
  • Если вы обращаетесь к элементу __annotations__ объекта напрямую, перед анализом его содержимого убедитесь, что это словарь.
  • Не следует изменять словари __annotations__.
  • Не следует удалять атрибут __annotations__ объекта.

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

Во всех версиях Python 3 объекты-функции лениво создают словарь аннотаций, если у объекта не определены аннотации. Атрибут __annotations__ можно удалить с помощью del fn.__annotations__, однако при последующем обращении к fn.__annotations__ объект создаст новый пустой словарь, сохранит его и вернёт в качестве своих аннотаций. Удаление аннотаций функции до того, как она лениво создала свой словарь аннотаций, вызовет AttributeError; гарантируется, что повторное использование del fn.__annotations__ приведёт к AttributeError.

Всё сказанное в предыдущем абзаце также относится к объектам классов и модулей в Python 3.10 и новее.

Во всех версиях Python 3 атрибуту __annotations__ объекта-функции можно присвоить значение None. Однако при последующем обращении к аннотациям объекта с помощью fn.__annotations__ будет лениво создан пустой словарь, как описано в первом абзаце этого раздела. В любой версии Python это не относится к модулям и классам: этим объектам можно присвоить любое значение Python в качестве __annotations__, и они сохранят присвоенное значение.

Если Python сам преобразует ваши аннотации в строки (с помощью from __future__ import annotations), а вы укажете строку в качестве аннотации, эта строка сама будет заключена в кавычки. Фактически аннотация будет заключена в кавычки дважды. Например:

from __future__ import annotations
def foo(a: "str"): pass

print(foo.__annotations__)

Будет выведено {'a': "'str'"}. Это не совсем следует считать «особенностью»; здесь об этом сказано только потому, что такое поведение может удивить.

© 2001–2025 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.13/howto/annotations.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API