Рекомендации по работе с аннотациями
- автор:
-
Larry Hastings
Доступ к словарю аннотаций объекта в 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