Давайте пофантазируем вновь: представим, что нам хочется иметь возможность валидировать аргументы (проверять соответствие их значений неким правилам) функций. И хочется это делать с помощью декораторов, которые можно применять раз за разом. Парочку таких декораторов мы и реализуем к концу урока. Но для начала нужно слегка отвлечься.
Что будет, если аргументы функции не проходят нашу проверку? Нам нужно показывать ошибку. Но как это сделать? Подробнее о работе с ошибками будет рассказано в последующих курсах, а пока просто покажу, как спровоцировать ошибку:
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
. Выглядит сложновато, но вы со временем научитесь сразу писать и читать такой код, ведь декораторы — в т.ч. и с параметрами — частые гости в коде на Python.
Применение декоратора с параметрами выглядит так:
@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
Условия выглядят почти как фразы на разговорном английском, не правда ли?! Каждая "фабрика предикатов" (ФВП, возвращающая предикат) получилась достаточно абстрактной, чтобы быть применимой для валидации разных значений. А ещё наши предикаты композируемы — удобны для создания комбинаций из существующих функций без написания новых (мне лично нравится not_(in_(…))
!).
Когда мы объявляем функцию, то функция получает имя. А ещё функция может иметь строку документации или 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
сделает ваши декораторы достойными представителями вида, всегда используйте его!
Вам ответят команда поддержки Хекслета или другие студенты.
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.
Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно
Наши выпускники работают в компаниях:
С нуля до разработчика. Возвращаем деньги, если не удалось найти работу.
Зарегистрируйтесь или войдите в свой аккаунт