Лучшие методы работы с аннотациями¶
- автор
Ларри Гастингс
Аннотация
Этот документ предназначен для описания лучших практик работы с аннотационными матрицами. Если вы пишете код на 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.9 и старше¶
В Python 3.9 и более старых версиях доступ к дикту аннотаций объекта намного сложнее, чем в более новых версиях. Проблема заключается в недостатках дизайна этих старых версий Python, в частности, в аннотациях классов.
Лучшая практика для доступа к дикту аннотаций других объектов - функций, других вызываемых объектов и модулей - такая же, как и лучшая практика для 3.10, при условии, что вы не вызываете
inspect.get_annotations()
: вы должны использовать трехаргументныйgetattr()
для доступа к атрибуту объекта__annotations__
.К сожалению, это не лучшая практика для классов. Проблема в том, что, поскольку
__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__
, объект создаст новый пустой dict, который он будет хранить и возвращать в качестве своих аннотаций. Удаление аннотаций функции до того, как она лениво создаст свою дикту аннотаций, приведет к выбросуAttributeError
; использованиеdel fn.__annotations__
дважды подряд гарантированно всегда приводит к выбросуAttributeError
.Все, что написано выше, относится и к объектам классов и модулей в Python 3.10 и новее.
Во всех версиях Python 3 вы можете установить
__annotations__
на объекте функции значениеNone
. Однако последующий доступ к аннотациям этого объекта с помощьюfn.__annotations__
приведет к ленивому созданию пустого словаря, как указано в первом абзаце этого раздела. Это не относится к модулям и классам, в любой версии Python; эти объекты позволяют установить__annotations__
в любое значение Python, и сохранят любое установленное значение.Если Python строчит аннотации для вас (используя
from __future__ import annotations
), и вы указываете строку в качестве аннотации, то сама строка будет заключена в кавычки. В результате аннотация будет заключена в кавычки дважды. Например:from __future__ import annotations def foo(a: "str"): pass print(foo.__annotations__)Это выводит
{'a': "'str'"}
. Это не следует считать «причудой»; это упомянуто здесь просто потому, что это может удивить.