Spec-Zone.ru › Python 3.14

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

автор:

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–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 и старше

В Python 3.9 и старше доступ к словарю аннотаций объекта гораздо сложнее, чем в новых версиях. Проблема связана с недостатком проектирования в этих старых версиях 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 имеет значение true.

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

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

  • Не следует напрямую присваивать значения члену __annotations__ объектов. Предоставьте Python самому управлять установкой __annotations__.
  • Если вы напрямую присваиваете значение члену __annotations__ объекта, всегда устанавливайте его в объект типа dict.
  • Не следует обращаться к __annotations__ какого-либо объекта напрямую. Вместо этого используйте annotationlib.get_annotations() (Python 3.14+) или inspect.get_annotations() (Python 3.10+).
  • Если вы обращаетесь к члену __annotations__ объекта напрямую, перед проверкой содержимого убедитесь, что это словарь.
  • Не следует изменять словари __annotations__.
  • Не следует удалять атрибут __annotations__ объекта.

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

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

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

Во всех версиях Python 3 можно присвоить None атрибуту __annotations__ объекта-функции. Однако последующее обращение к аннотациям этого объекта с помощью 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+).

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

Spec-Zone.ru

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