Russian (Pусский) translation by Ilya Nikov (you can also view the original English article)
Аннотации функций - это функция Python 3, которая позволяет добавлять произвольные метаданные к аргументам функции и возвращаемому значению. Они были частью оригинальной спецификации Python 3.0.
В этом уроке я покажу вам, как использовать аннотации функций общего назначения и объединить их с декораторами. Вы также узнаете о преимуществах и недостатках аннотаций функций, когда их целесообразно использовать, и когда лучше использовать другие механизмы, такие как докстринги и простые декораторы.
Аннотации функций
Аннотации функций указаны в PEP-3107. Основная мотивация заключалась в том, чтобы обеспечить стандартный способ связывания метаданных с аргументами функции и возвращаемым значением. Многие члены сообщества обнаружили новые варианты использования, но использовали различные методы, такие как пользовательские декораторы, пользовательские форматы docstring и добавление пользовательских атрибутов к объекту функции.
Важно понимать, что Python не благословляет аннотации какой-либо семантикой. Он всего лишь обеспечивает синтаксическую поддержку для связывания метаданных, а также простой способ доступа к ней. Кроме того, аннотации полностью необязательны.
Давайте посмотрим на пример. Вот функция foo(), которая принимает три аргумента, называемых a, b и c, и печатает их сумму. Обратите внимание: foo() ничего не возвращает. Первый аргумент a не аннотируется. Второй аргумент b аннотируется строкой 'annotating b', а третий аргумент c аннотируется с типом int. Возвращаемое значение аннотируется типом float. Обратите внимание на синтаксис «->» для аннотирования возвращаемого значения.
1 |
def foo(a, b: 'annotating b', c: int) -> float: |
2 |
print(a + b + c) |
Аннотации не влияют на выполнение функции. Давайте назовем foo() дважды: один раз с аргументами int и один раз с строковыми аргументами. В обоих случаях foo() делает правильную вещь, и аннотации просто игнорируются.
1 |
foo('Hello', ', ', 'World!') |
2 |
Hello, World! |
3 |
|
4 |
foo(1, 2, 3) |
5 |
6
|
Аргументы по умолчанию
Аргументы по умолчанию указываются после аннотации:
1 |
def foo(x: 'an argument that defaults to 5' = 5): |
2 |
print(x) |
3 |
|
4 |
foo(7) |
5 |
7
|
6 |
|
7 |
foo() |
8 |
5
|
Доступ к аннотации функций
Объект функции имеет атрибут 'annotations'. Это сопоставление, сопоставляющее каждое имя аргумента с его аннотацией. Аннотации возвращаемого значения сопоставляются с ключом «return», который не может конфликтовать с любым именем аргумента, потому что «return» является зарезервированным словом, которое не может служить именем аргумента. Обратите внимание, что можно передать аргумент ключевого слова с именем return в функцию:
1 |
def bar(*args, **kwargs: 'the keyword arguments dict'): |
2 |
print(kwargs['return']) |
3 |
|
4 |
d = {'return': 4} |
5 |
bar(**d) |
6 |
4
|
Вернемся к нашему первому примеру и отметьте его аннотации:
1 |
def foo(a, b: 'annotating b', c: int) -> float: |
2 |
print(a + b + c) |
3 |
|
4 |
print(foo.__annotations__) |
5 |
{'c': <class 'int'>, 'b': 'annotating b', 'return': <class 'float'>} |
Это довольно просто. Если вы аннотируете функцию с массивом аргументов и / или массивом аргументов аргументов, то, очевидно, вы не можете аннотировать отдельные аргументы.
1 |
def foo(*args: 'list of unnamed arguments', **kwargs: 'dict of named arguments'): |
2 |
print(args, kwargs) |
3 |
|
4 |
print(foo.__annotations__) |
5 |
{'args': 'list of unnamed arguments', 'kwargs': 'dict of named arguments'} |
Если вы прочитали раздел о доступе к аннотации функций в PEP-3107, он говорит, что вы обращаетесь к ним через атрибут func_annotations объекта функции. Для Python 3.2 это устарело. Не путайте. Это просто атрибут annotations.
Что вы можете сделать с аннотациями?
Это большой вопрос. Аннотации не имеют стандартного значения или семантики. Существует несколько категорий общих применений. Вы можете использовать их в качестве лучшей документации и переместить аргумент и вернуть значение документации из docstring. Например, эта функция:
1 |
def div(a, b): |
2 |
"""Divide a by b
|
3 |
args:
|
4 |
a - the dividend
|
5 |
b - the divisor (must be different than 0)
|
6 |
return:
|
7 |
the result of dividing a by b
|
8 |
"""
|
9 |
return a / b |
Может быть преобразована в:
1 |
def div(a: 'the dividend', |
2 |
b: 'the divisor (must be different than 0)') -> 'the result of dividing a by b': |
3 |
"""Divide a by b"""
|
4 |
return a / b |
При сохранении той же информации в версии аннотаций есть несколько преимуществ:
- Если вы переименуете аргумент, версия docstring документации может быть устаревшей.
- Легче понять, не аргументирован ли аргумент.
- Нет необходимости придумывать специальный формат аргументационной документации внутри docstring для анализа инструментами. Атрибут annotations обеспечивает прямой, стандартный механизм доступа.
Другое использование, о котором мы поговорим позже, - это необязательный ввод текста. Python динамически типизирован, что означает, что вы можете передать любой объект в качестве аргумента функции. Но часто функции требуют, чтобы аргументы имели определенный тип. С аннотациями вы можете указать тип прямо рядом с аргументом очень естественным образом.
Помните, что просто указание типа не будет обеспечивать его выполнение, и потребуется дополнительная работа (большая работа). Тем не менее, даже просто указание типа может сделать намерение более понятным, чем указание типа в docstring, и это может помочь пользователям понять, как вызвать функцию.
Еще одно преимущество аннотаций над docstring заключается в том, что вы можете прикреплять различные типы метаданных в виде кортежей или dicts. Опять же, вы можете сделать это с помощью docstring, но он будет основан на тексте и потребует специального анализа.
Наконец, вы можете приложить много метаданных, которые будут использоваться специальными внешними инструментами или во время выполнения через декораторы. Я рассмотрю этот вариант в следующем разделе.
Несколько аннотаций
Предположим, вы хотите аннотировать аргумент как своим типом, так и строкой справки. Это очень просто с аннотациями. Вы можете просто аннотировать аргумент с помощью dict, который имеет два ключа: «type» и «help».
1 |
def div(a: dict(type=float, help='the dividend'), |
2 |
b: dict(type=float, help='the divisor (must be different than 0)') |
3 |
) -> dict(type=float, help='the result of dividing a by b'): |
4 |
"""Divide a by b"""
|
5 |
return a / b |
6 |
|
7 |
print(div.__annotations__) |
8 |
{'a': {'help': 'the dividend', 'type': float}, |
9 |
'b': {'help': 'the divisor (must be different than 0)', 'type': float}, |
10 |
'return': {'help': 'the result of dividing a by b', 'type': float}} |
Комбинирование аннотаций и декораторов Python
Аннотации и декораторы идут рука об руку. Для хорошего ознакомления с декораторами Python ознакомьтесь со своими двумя учебниками: Погружаемся в декораторы Python и Пишем свои собственные декораторы Python.
Во-первых, аннотации могут быть полностью реализованы в качестве декораторов. Вы можете просто определить декоратор @annotate и взять его аргумент и выражение Python в качестве аргументов, а затем сохранить в атрибуте annotations целевой функции. Это можно сделать и для Python 2.
Однако реальная сила декораторов заключается в том, что они могут воздействовать на аннотации. Это требует координации, конечно, о семантике аннотаций.
Давайте посмотрим на примере. Предположим, мы хотим проверить, что аргументы находятся в определенном диапазоне. Аннотирование будет кортежем с минимальным и максимальным значением для каждого аргумента. Затем нам нужен декоратор, который будет проверять аннотацию каждого аргумента ключевого слова, проверять, что значение находится в пределах диапазона, и в противном случае кидать исключение. Начнем с декоратора:
1 |
def check_range(f): |
2 |
def decorated(*args, **kwargs): |
3 |
for name, range in f.__annotations__.items(): |
4 |
min_value, max_value = range |
5 |
if not (min_value <= kwargs[name] <= max_value): |
6 |
msg = 'argument {} is out of range [{} - {}]' |
7 |
raise ValueError(msg.format(name, min_value, max_value)) |
8 |
return f(*args, **kwargs) |
9 |
return decorated |
Теперь давайте определим нашу функцию и украсим ее декораторами @check_range.
1 |
@check_range |
2 |
def foo(a: (0, 8), b: (5, 9), c: (10, 20)): |
3 |
return a * b - c |
Вызовем foo() с разными аргументами и посмотрим, что произойдет. Когда все аргументы находятся в пределах их диапазона, проблем нет.
1 |
foo(a=4, b=6, c=15) |
2 |
9
|
Но если мы установим c в 100 (вне диапазона (10, 20)), то возникает исключение:
1 |
foo(a=4, b=6, c=100) |
2 |
ValueError: argument c is out of range [10 - 20] |
Когда следует использовать декораторы вместо аннотаций?
Есть несколько ситуаций, когда декораторы лучше, чем аннотации для прикрепления метаданных.
Один очевидный случай: если ваш код должен быть совместим с Python 2.
Другой случай - если у вас много метаданных. Как вы видели ранее, хотя можно добавить любое количество метаданных с помощью dicts в виде аннотаций, это довольно громоздко и на самом деле вредит читабельности.
Наконец, если метаданные должны использоваться определенным декоратором, может быть лучше связать метаданные в качестве аргументов самого декоратора.
Динамические аннотации
Аннотации - это просто атрибут dict функции.
1 |
type(foo.__annotations__) |
2 |
dict
|
Это означает, что вы можете изменять их на лету во время работы программы. Каковы некоторые варианты использования? Предположим, вы хотите узнать, используется ли значение аргумента по умолчанию для аргумента по умолчанию. Всякий раз, когда функция вызывается со значением по умолчанию, вы можете увеличить значение аннотации. Или, может быть, вы хотите суммировать все возвращаемые значения. Динамический аспект можно сделать внутри самой функции или декоратора.
1 |
def add(a, b) -> 0: |
2 |
result = a + b |
3 |
add.__annotations__['return'] += result |
4 |
return result |
5 |
|
6 |
print(add.__annotations__['return']) |
7 |
0
|
8 |
|
9 |
add(3, 4) |
10 |
7
|
11 |
print(add.__annotations__['return']) |
12 |
7
|
13 |
|
14 |
add(5, 5) |
15 |
10
|
16 |
print(add.__annotations__['return']) |
17 |
17
|
Заключение
Аннотации функций универсальны и увлекательны. У них есть потенциал, чтобы вступить в новую эру интроспективных инструментов, которые помогут разработчикам освоить все более сложные системы. Они также предлагают более продвинутому разработчику стандартный и читаемый способ непосредственного связывания метаданных с аргументами и возвращаемого значения для создания пользовательских инструментов и взаимодействия с декораторами. Но чтобы выиграть от этого нужно будет потрудиться.



