Spec-Zone.ru › Python 3.11

Рекомендации по использованию аннотаций

author

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

Аннотация

Этот документ предназначен для описания лучших практик работы с аннотациями словарей. Если вы пишете код 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.10 доступ к __annotations__ для класса, не определяющего аннотаций, но имеющего родительский класс с аннотациями, возвращал аннотации родительского класса. В Python 3.10 и новее аннотации дочернего класса будут пустым словарем.

Доступ к словарю аннотаций объекта в 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

В ситуациях, когда некоторые аннотации могут быть «строковыми», и вы хотите оценить эти строки для получения представляемых ими значений 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__ объекта.
END_OF_DOCUMENT_MARKER

__annotations__ Странности

Во всех версиях Python 3 объекты функций лениво создают словарь аннотаций, если на этом объекте не определены аннотации. Вы можете удалить атрибут __annotations__ с помощью del fn.__annotations__, но если затем вы обратитесь к fn.__annotations__, объект создаст новый пустой словарь, который он сохранит и вернёт в качестве своих аннотаций. Удаление аннотаций у функции до того, как она лениво создала свой словарь аннотаций, вызовет 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'"}. Это не следует рассматривать как настоящую «странность»; это упомянуто здесь просто потому, что это может быть неожиданным.

© 2001–2023 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.11/howto/annotations.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API