Рекомендации по использованию аннотаций
- автор:
-
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— модуль, используйте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