Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

Больше о декораторах Python: Функции

В этом уроке мы научимся создавать декораторы с параметрами. Мы узнаем, как создавать декоратор так, чтобы сохранялись документация и прочие свойства оборачиваемой функции.

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

Представим, что нам нужно валидировать аргументы функций — проверять соответствие их значений неким правилам. Это можно сделать с помощью декораторов, которые можно применять на постоянной основе. Такие декораторы мы и реализуем в этом уроке.

Аргументы функции могут не проходить проверку на соответствие правилам. В этом случае нам нужно показывать эту ошибку. Посмотрим, как спровоцировать ошибку:

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 сделает декораторы достойными представителями вида.

Выводы

В этом уроке мы научились создавать декораторы с параметрами. В итоге мы узнали, как создавать декоратор так, чтобы сохранялись документация и прочие свойства оборачиваемой функции.


Дополнительные материалы

  1. Продвинутые возможности Python: замыкания, декораторы

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Об обучении на Хекслете

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы
профессия
от 6 300 ₽ в месяц
Разработка веб-приложений на Django
10 месяцев
с нуля
Старт 25 апреля

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»