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


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

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

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

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

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

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

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

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

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

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

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы

С нуля до разработчика. Возвращаем деньги, если не удалось найти работу.

Иконка программы Python-разработчик
Профессия
Разработка веб-приложений на Django
30 июня 10 месяцев

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

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

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

Даю согласие на обработку персональных данных, соглашаюсь с «Политикой конфиденциальности» и «Условиями оказания услуг»