Аннотации к лучшим практикам

автор:

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

Доступ К Аннотациям Объекта В Python 3.10 И Более Поздних Версиях

В Python 3.10 в стандартную библиотеку добавлена новая функция: inspect.get_annotations(). В версиях Python 3.10 и новее вызов этой функции является наилучшей практикой для доступа к annotations dict любого объекта, который поддерживает аннотации. Эта функция также может «расшифровать» аннотации в виде строк для вас.

Если по какой-либо причине 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 и более поздних версиях аннотации дочернего класса вместо этого будут представлять собой пустой dict.

Доступ К Аннотациям Объекта В 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 в классе dict.

Чтобы свести все это воедино, вот несколько примеров кода, который безопасно обращается к атрибуту __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 имеет значение true.

Если eval() попытается вычислить такие значения, произойдет сбой и возникнет исключение. Таким образом, при разработке библиотечного API, работающего с аннотациями, рекомендуется пытаться вычислять строковые значения только по явному запросу вызывающей стороны.

Рекомендации для __annotations__ В Любой версии Python

  • Вам следует избегать прямого назначения __annotations__ элементу objects. Позвольте Python управлять настройкой __annotations__.

  • Если вы присваиваете значение непосредственно элементу __annotations__ объекта, вы всегда должны присваивать ему значение dict объекта.

  • Если вы напрямую обращаетесь к элементу __annotations__ объекта, вы должны убедиться, что это словарь, прежде чем пытаться изучить его содержимое.

  • Вам следует избегать изменения __annotations__ символов.

  • Вам следует избегать удаления атрибута __annotations__ объекта.

__annotations__ Причуды

Во всех версиях Python 3 функция objects lazy-создает dict аннотаций, если для этого объекта не определены аннотации. Вы можете удалить атрибут __annotations__, используя del fn.__annotations__, но если вы затем получите доступ к fn.__annotations__, объект создаст новый пустой dict, который он сохранит и вернет в качестве своих аннотаций. Удаление аннотаций в функции до того, как она лениво создаст свой annotations dict, приведет к появлению AttributeError; использование del fn.__annotations__ дважды подряд гарантированно всегда приведет к появлению AttributeError.

Все, что описано в приведенном выше абзаце, также применимо к объектам class и module в Python 3.10 и более поздних версиях.

Во всех версиях Python 3 вы можете установить для __annotations__ объекта function значение 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'"}. На самом деле это не следует считать «причудой»; это упомянуто здесь просто потому, что это может вызвать удивление.

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