Рекомендации по использованию аннотаций
- 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, лучше всего вызвать 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имеет значение 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–2023 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.10/howto/annotations.html