Аннотации к лучшим практикам¶
- автор:
Ларри Хастингс
Доступ К Аннотациям Объекта В Python 3.10 И Более Поздних Версиях¶
В Python 3.10 в стандартную библиотеку добавлена новая функция: inspect.get_annotations()
. В версиях Python 3.10 и новее вызов этой функции является наилучшей практикой для доступа к annotations dict любого объекта, который поддерживает аннотации. Эта функция также может «расшифровать» аннотации в виде строк для вас.
Если по какой-либо причине 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
в классе dict.
Чтобы свести все это воедино, вот несколько примеров кода, который безопасно обращается к атрибуту __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
имеет значение true.
Если eval()
попытается вычислить такие значения, произойдет сбой и возникнет исключение. Таким образом, при разработке библиотечного API, работающего с аннотациями, рекомендуется пытаться вычислять строковые значения только по явному запросу вызывающей стороны.
Рекомендации для __annotations__
В Любой версии Python¶
Вам следует избегать прямого назначения
__annotations__
элементу objects. Позвольте Python управлять настройкой__annotations__
.Если вы присваиваете значение непосредственно элементу
__annotations__
объекта, вы всегда должны присваивать ему значениеdict
объекта.Если вы напрямую обращаетесь к элементу
__annotations__
объекта, вы должны убедиться, что это словарь, прежде чем пытаться изучить его содержимое.Вам следует избегать изменения
__annotations__
символов.Вам следует избегать удаления атрибута
__annotations__
объекта.
__annotations__
Причуды¶
Во всех версиях Python 3 функция objects lazy-создает dict аннотаций, если для этого объекта не определены аннотации. Вы можете удалить атрибут __annotations__
, используя del fn.__annotations__
, но если вы затем получите доступ к fn.__annotations__
, объект создаст новый пустой dict, который он сохранит и вернет в качестве своих аннотаций. Удаление аннотаций в функции до того, как она лениво создаст свой annotations dict, приведет к появлению AttributeError
; использование del fn.__annotations__
дважды подряд гарантированно всегда приведет к появлению AttributeError
.
Все, что описано в приведенном выше абзаце, также применимо к объектам class и module в Python 3.10 и более поздних версиях.
Во всех версиях Python 3 вы можете установить для __annotations__
объекта function значение 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'"}
. На самом деле это не следует считать «причудой»; это упомянуто здесь просто потому, что это может вызвать удивление.