Содержание страницы
Лучшие практики работы с аннотациями¶Annotations Best Practices
- автор
Larry Hastings
Аннотация
Этот документ предназначен для того, чтобы собрать лучшие практики работы со словарями аннотаций.
Если вы пишете код на Python, который анализирует __annotations__ у объектов Python, мы рекомендуем следовать приведённым ниже рекомендациям.
Документ состоит из четырёх разделов:
лучшие практики доступа к аннотациям объекта в Python версии 3.10 и новее,
лучшие практики доступа к аннотациям объекта в Python версии 3.9 и старше,
другие лучшие практики для __annotations__, применимые к любой версии Python,
и
особенности __annotations__.
Обратите внимание, что этот документ посвящён работе с __annotations__, а не об использовании для аннотаций.
Если вы ищете информацию о том, как использовать «подсказки типов» в своём коде, обратитесь к модулю typing.
Доступ к словарю аннотаций объекта в Python 3.10 и новее¶Accessing The Annotations Dict Of An Object In Python 3.10 And Newer
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__у класса, который не определяет аннотации, но имеет родительский класс с аннотациями, возвращал__annotations__родителя. В Python 3.10 и новее аннотации дочернего класса вместо этого будут пустым словарём.
Доступ к словарю аннотаций объекта в Python 3.9 и старше¶Accessing The Annotations Dict Of An Object In Python 3.9 And Older
В 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__.
Ручное обратное преобразование строковых аннотаций¶Manually Un-Stringizing Stringized Annotations
В ситуациях, когда некоторые аннотации могут быть «строкизированы» и требуется вычислить эти строки, чтобы получить представляемые ими значения 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¶Best Practices For __annotations__ In Any Python Version
Следует избегать прямого присваивания атрибуту
__annotations__объектов. Управление установкой__annotations__следует оставить Python.Если всё же выполняется прямое присваивание атрибуту
__annotations__объекта, всегда следует устанавливать его равным объектуdict.При прямом доступе к атрибуту
__annotations__объекта следует убедиться, что он является словарём, прежде чем пытаться просмотреть его содержимое.Следует избегать изменения словарей
__annotations__.Следует избегать удаления атрибута
__annotations__объекта.
__annotations__ Особенности¶__annotations__ Quirks
Во всех версиях 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'"}. Это не стоит считать «странностью»; это упоминается здесь просто потому, что может показаться неожиданным.