Больше о декораторах

Декораторы с параметрами

Давайте пофантазируем вновь: представим, что нам хочется иметь возможность валидировать аргументы (проверять соответствие их значений неким правилам) функций. И хочется это делать с помощью декораторов, которые можно применять раз за разом. Парочку таких декораторов мы и реализуем к концу урока. Но для начала нужно слегка отвлечься.

Что будет, если аргументы функции не проходят нашу проверку? Нам нужно показывать ошибку. Но как это сделать? Подробнее о работе с ошибками будет расказано в последующих курсах, а пока просто покажу, как спровоцировать ошибку:

>>> 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 сделает ваши декораторы достойными представителями вида, всегда используйте его!

Мы учим программированию с нуля до стажировки и работы. Попробуйте наш бесплатный курс «Введение в программирование» или полные программы обучения по Javascript, PHP, Python и Java.

Хекслет

Подробнее о том, почему наше обучение работает →