Лучшие методы работы с аннотациями

автор

Ларри Гастингс

Аннотация

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

Вернуться на верх