Spec-Zone.ru › Python 3.12

Рекомендации по использованию аннотаций

автор:

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 — модуль, используйте 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__ в качестве глобального пространства имён при вызове eval().

Однако не все строковые значения, используемые в качестве аннотаций, можно успешно преобразовать в значения 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; гарантированно вызовет AttributeError и повторный вызов del fn.__annotations__.

Всё сказанное выше также относится к объектам классов и модулей в 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'"}. Это не совсем «особенность»; мы упоминаем об этом лишь потому, что такое поведение может удивить.

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

Spec-Zone.ru

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