Рекомендации по работе с аннотациями
- author:
-
Ларри Хэстингс
Доступ к словарю аннотаций объекта в 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равно true.
Если eval() попытается оценить такие значения, это завершится ошибкой и будет выброшено исключение. Поэтому при проектировании API библиотеки, работающей с аннотациями, рекомендуется пытаться оценивать строковые значения только при явном запросе вызывающей стороны.
Лучшие практики для __annotations__ в любой версии Python
- Следует избегать прямого присваивания атрибуту
__annotations__объектов. Позвольте Python самостоятельно управлять установкой__annotations__. - Если вы всё же присваиваете атрибуту
__annotations__объекта напрямую, вы всегда должны устанавливать его в значение типаdict. - Если вы напрямую получаете доступ к атрибуту
__annotations__объекта, вы должны убедиться, что это словарь, прежде чем пытаться проанализировать его содержимое. - Следует избегать изменения словарей
__annotations__. - Следует избегать удаления атрибута
__annotations__объекта.
__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–2024 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.13/howto/annotations.html