Лучшие методы работы с аннотациями¶
- автор
Ларри Гастингс
Аннотация
Этот документ предназначен для описания лучших практик работы с аннотационными матрицами. Если вы пишете код на 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'"}. Это не следует считать «причудой»; это упомянуто здесь просто потому, что это может удивить.