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