В этом уроке мы научимся создавать декораторы с параметрами. Мы узнаем, как создавать декоратор так, чтобы сохранялись документация и прочие свойства оборачиваемой функции.
Декораторы с параметрами
Представим, что нам нужно валидировать аргументы функций — проверять соответствие их значений неким правилам. Это можно сделать с помощью декораторов, которые можно применять на постоянной основе. Такие декораторы мы и реализуем в этом уроке.
Аргументы функции могут не проходить проверку на соответствие правилам. В этом случае нам нужно показывать эту ошибку. Посмотрим, как спровоцировать ошибку:
raise ValueError('Value too low!')
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# ValueError: Value too low!
Такую ошибку мы и будем показывать, если значение аргумента не проходит валидацию. А теперь можно создавать декораторы.
Допустим, числовой аргумент функции должен быть больше нуля и не равен «плохим» значениям. Например, в таком случае мы могли бы сделать специальные декораторы следующего вида:
@greater_than_zero
@not_bad
def function(arg):
# …
Проблема такого способа в том, что таких узкоспециализированных декораторов нельзя будет собрать на все случаи. Нам нужно отделить оборачивание функции и сами проверки, чтобы в роли последних могли выступать обычные предикаты. Но как декоратор узнает о предикате, если всегда принимает единственный параметр — оборачиваемую функцию?
Это можно сделать через замыкание. В таком случае нам понадобится функция, которая примет в качестве аргумента функцию-предикат и вернет функцию-обертку. Последняя тоже примет в качестве аргумента функцию и вернет функцию.
Напишем такой код:
def checking_that_arg_is(predicate, error_message):
def wrapper(function):
def inner(arg):
if not predicate(arg):
raise ValueError(error_message)
return function(arg)
return inner
return wrapper
Функция checking_that_arg_is
принимает предикат и возвращает wrapper
.
wrapper
— это декоратор с inner
внутри. Он проверяет аргумент предикатом. Если условие соблюдается, то он вызывает function
.
Применение декоратора с параметрами выглядит так:
@checking_that_arg_is(condition, "Invalid value!")
def foo(arg):
# …
Теперь у нас есть чем оборачивать. Напишем несколько замыканий, которые выступят проверками:
def greater_than(value):
def predicate(arg):
return arg > value
return predicate
def in_(*values):
def predicate(arg):
return arg in values
return predicate
def not_(other_predicate):
def predicate(arg):
return not other_predicate(arg)
return predicate
У функций not_
и in_
в конце названия есть символ _
. Так принято называть переменные, имена которых совпадают с ключевыми словами или именами встроенных функций.
Эти ФВП принимают параметры и возвращают предикаты, которые удобно использовать с описанным выше декоратором.
Напомним, что нам нужно проверять, что у аргумента функции есть значение, которое больше нуля и не равно «плохим» значениям. Так эти условия будут выглядеть в коде:
@checking_that_arg_is(greater_than(0), "Non-positive!")
@checking_that_arg_is(not_(in_(5, 15, 42)), "Bad value!")
def foo(arg):
return arg
foo(0)
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# File "<stdin>", line 5, in inner
# ValueError: Non-positive!
foo(5)
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# File "<stdin>", line 6, in inner
# File "<stdin>", line 5, in inner
# ValueError: Bad value!
foo(6)
# 6
Условия выглядят почти как фразы на разговорном английском. Каждая ФВП, возвращающая предикат, получилась абстрактной, чтобы быть применимой для валидации разных значений. А еще предикаты получились композируемыми — удобными, чтобы создавать комбинации из существующих функций без написания новых.
Оборачиваем функции правильно
Когда мы объявляем функцию, то она получает имя. А еще у нее может быть строка документации или docstring. Эту документацию показывают разные инструменты, например, IDE или функция help()
в Python REPL:
def add_one(arg):
"""
Add one to argument.
Argument should be a number.
"""
return arg + 1
add_one
# <function add_one at 0x7f105936cd08>
# ^ вот и имя у объекта функции!
help(add_one)
# …
# add_one(arg)
# Add one to argument.
# Argument should be a number.
# …
Теперь посмотрим, что будет, если мы обернем функцию с помощью декоратора:
def wrapped(function):
def inner(arg):
return function(arg)
return inner
add_one = wrapped(add_one)
add_one
# <function wrapped.<locals>.inner at 0x7f1056f041e0>
help(add_one)
# …
# inner(arg)
# …
Функция потеряла имя — теперь это wrapped.<locals>.inner
. Также она потеряла документацию. Но нам нужно сохранить и то, и другое. Например, это можно сделать вручную — скопировать у оригинальной функции атрибуты __name__
и __doc__
. Но есть способ лучше.
Перепишем наш декоратор с помощью декоратора wraps
из модуля functools
:
from functools import wraps
def wrapped(function):
@wraps(function)
def inner(arg):
return function(arg)
return inner
def foo(_):
"""Bar."""
return 42
foo = wrapped(foo)
foo
# <function foo at 0x7f1057b15048>
help(foo)
# …
# foo()
# Bar.
# …
Мы обернули функцию foo
, но обертка сохранила документацию и имя. При этом wraps
— тоже декоратор с параметром.
У оберток, созданных с применением wraps
, есть еще одно полезное свойство. До обернутой функции можно всегда «достучаться» впоследствии: ссылка на оригинальную функцию хранится у обертки в атрибуте __wrapped__
:
foo.__wrapped__
# <function foo at 0x7f1056f04158>
В итоге декоратор wraps
сделает декораторы достойными представителями вида.
Выводы
В этом уроке мы научились создавать декораторы с параметрами. В итоге мы узнали, как создавать декоратор так, чтобы сохранялись документация и прочие свойства оборачиваемой функции.
Дополнительные материалы
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
- Статья «Как учиться и справляться с негативными мыслями»
- Статья «Ловушки обучения»
- Статья «Сложные простые задачи по программированию»
- Вебинар «Как самостоятельно учиться»
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.