Содержание страницы
Лучшие практики работы с аннотациями¶Annotations Best Practices
- автор:
Larry Hastings
Доступ к словарю аннотаций объекта в 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 по 3.13 вызов этой функции является лучшей практикой для доступа к словарю аннотаций любого объекта, поддерживающего аннотации. Эта функция также может преобразовывать строковые аннотации обратно в исходный вид.
В Python 3.14 появился новый модуль annotationlib с функциональностью для работы с аннотациями. Он включает функцию annotationlib.get_annotations(), которая заменяет inspect.get_annotations().
Если по какой-то причине 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__следует передать как 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__у любого объекта. Вместо этого следует использоватьannotationlib.get_annotations()(Python 3.14+) илиinspect.get_annotations()(Python 3.10+).Если всё же выполняется прямой доступ к атрибуту
__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'"}. Это не стоит считать «странностью»; это упоминается здесь просто потому, что может показаться неожиданным.
Если используется класс с пользовательским метаклассом и выполняется доступ к __annotations__ у этого класса, может наблюдаться неожиданное поведение; см. 749 для примеров. Этих особенностей можно избежать, используя annotationlib.get_annotations() в Python 3.14+ или inspect.get_annotations() в Python 3.10+. В более старых версиях Python этих ошибок можно избежать, обращаясь к аннотациям через __dict__ класса (например, cls.__dict__.get('__annotations__', None)).
В некоторых версиях Python экземпляры классов могут иметь атрибут __annotations__. Однако это не поддерживаемая функциональность. Если нужны аннотации экземпляра, можно использовать type() для доступа к его классу (например, annotationlib.get_annotations(type(myinstance)) в Python 3.14+).