Лучшие практики использования аннотаций¶
- автор:
Ларри Гастингс
Доступ к дикте аннотаций объекта в 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__
в классе, который не содержит аннотаций, но имеет родительский класс с аннотациями, возвращало родительский __annotations__
. В Python 3.10 и более новых версиях вместо аннотаций дочернего класса будет использоваться пустой dict.
Доступ к диктофону аннотаций объекта в 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__
в качестве глобального объекта при вызовеeval()
.
Однако не все строковые значения, используемые в качестве аннотаций, могут быть успешно превращены в значения Python с помощью eval()
. Теоретически строковые значения могут содержать любую допустимую строку, и на практике существуют случаи использования подсказок типов, когда требуется аннотировать строковые значения, которые конкретно не могут быть оценены. Например:
Типы объединения PEP 604, использующие
|
, до того, как поддержка этого была добавлена в Python 3.10.Определения, которые не нужны во время выполнения, импортируются только при значении
typing.TYPE_CHECKING
.
Если eval()
попытается оценить такие значения, он потерпит неудачу и вызовет исключение. Поэтому при разработке API библиотеки, работающей с аннотациями, рекомендуется пытаться оценивать строковые значения только по явному запросу вызывающей стороны.
Лучшие практики для __annotations__
В любой версии Python¶
Не следует присваивать член
__annotations__
объектам напрямую. Пусть Python сам управляет назначением__annotations__
.Если вы присваиваете член
__annotations__
объекту напрямую, всегда устанавливайте его наdict
.Если вы напрямую обращаетесь к члену
__annotations__
объекта, вам следует убедиться, что это словарь, прежде чем пытаться изучить его содержимое.Вам следует избегать модификаций
__annotations__
dicts.Не следует удалять атрибут
__annotations__
объекта.
__annotations__
Причуды¶
Во всех версиях Python 3 объекты функций лениво создают дикт с аннотациями, если для этого объекта не определены аннотации. Вы можете удалить атрибут __annotations__
с помощью del fn.__annotations__
, но если вы затем обратитесь к fn.__annotations__
, объект создаст новый пустой dict, который он будет хранить и возвращать в качестве своих аннотаций. Удаление аннотаций функции до того, как она лениво создаст свою дикту аннотаций, вызовет ошибку 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'"}
. Это не следует считать «причудой»; она упомянута здесь просто потому, что может вызвать удивление.