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

Декораторы Python: Функции

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

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

Что такое декораторы

Ранее мы рассматривали как можно "запомнить" передаваемые значения в функцию с помощью замыканий. Напомним как выглядит синтаксис:

def outer(arg1):
    def inner(arg2):
        return arg1 + arg2
    return inner

two_add = outer(2)
two_add(1) # 3
two_add(3) # 5
two_add(5) # 7

В этом примере мы создаем функцию outer(), которая принимает аргумент arg1 и возвращает внутреннюю функцию inner(). Внутренняя функция принимает аргумент arg2 и возвращает сумму arg1 и agr2.

Может запутать одновременное определение функции и возврат ее же. Но если мы вспомним, что определение функции это присваивание функции имени переменной, а также, что функции это данные, то пример можно представить как:

def outer(arg1):
    inner = lambda arg2: arg1 + arg2
    return inner

Мы заменили именованную функцию лямбдой, и теперь присваивание ничем не отличается, как если бы мы присвоили переменной inner() число или строку.

Но если функции всего лишь данные, то можно ли их также "запоминать" в замыканиях?

def outer(func):
    def inner(arg):
        print('func = ', func)
        print('arg = ', arg)
        return func(arg)
    return inner

def square(x):
    return x ** 2

printer = outer(square)

printer(1) # 1
# => func =  <function square at 0x75e04623a7a0>
# => arg =  1

printer(2) # 4
# => func =  <function square at 0x75e04623a7a0>
# => arg =  2

printer(4) # 16
# => func =  <function square at 0x75e04623a7a0>
# => arg =  4

Остановимся и разберем пример выше. В нем мы создаем замыкание, но теперь, вместо числа в замыкание передается функция. Внутренняя функция запомнила переданную в нее и может использовать внутри. Осталось лишь передать аргументы и вызвать замкнутую функцию с ними. В дополнение, мы смогли распечатать переданную функцию и ее аргументы, при этом код самой функции square() мы не меняли.

Функции выше называются декораторами. Декораторы в Python — это функции, которые принимают другую функцию в качестве аргумента, добавляют к ней дополнительную функциональность и возвращают функцию с измененным поведением. Декораторы позволяют изменять поведение функций и классов с помощью добавления или изменения их функциональности без изменения самого кода.

Также, как вы могли заметить, декораторы это частный случай функции высшего порядка (принимаем функцию) и использования замыкания вместе.

Использование декораторов

Декораторы используются в Python повсеместно: для логирования, кеширования, добавления нового функционала, используются в тестовых и веб-фреймворках.

Предположим, что у нас есть функция, которая суммирует числа:

def sum(*nums):
    result = 0
    for num in nums:
        result += num
    return result

Создадим декоратор, который добавит к этой функции функциональность для отладки:

def debug_decorator(func):
    # внутреннюю функцию принято называть wrapper - обертка
    def wrapper(*args, **kwargs):
        print("Args:", args, kwargs)
        result = func(*args, **kwargs)
        print("Result:", result)
        return result
    return wrapper

Этот декоратор принимает функцию в качестве аргумента и возвращает новую функцию-обертку, которая добавляет отладочные сообщения в процесс выполнения исходной функции. Заметьте, что внутренняя функция принимает сразу все аргументы с помощью *args и **kwargs. Так мы можем создавать "всеядные" обертки.

Применим этот декоратор к функции sum():

debuged_sum = debug_decorator(sum)

debuged_sum(1, 2, 3) # 6

# => Args: (1, 2, 3) {}
# => Result: 6

Проблема в коде выше, что теперь функция называется debuged_sum(), и нужно будет изменить весь код, что использовал sum(). Но ведь имя функции это всего лишь имя переменной, и мы можем перезаписать новую, декорированную функцию в ту же переменную.

sum = debug_decorator(sum)

sum(1, 2, 3) # 6
# => Args: (1, 2, 3) {}
# => Result: 6

def sum_odd_numbers(nums):
    odd = []
    for num in nums:
        if num % 2 != 0:
            odd.append(num)
    return sum(*odd)

numbers = [1, 2, 3, 4, 5]
sum_odd_numbers(numbers) # 9
# => Args: (1, 3, 5) {}
# => Result: 9

Теперь функция sum() будет выполняться с дополнительными отладочными сообщениями. А также весь код, использующий функцию sum() будет использовать новую функцию.

Вместо того чтобы не добавлять запись вида func = decorator(func) каждый раз, в Python добавили синтаксический сахар - @decorator

# декоратор нужно добавить над определением функции, которую хотим декорировать
@debug_decorator
def sum(*nums):
    result = 0
    for num in nums:
        result += num
    return result

sum(1, 2) # 3
# => Args: (1, 2) {}
# => Result: 3

Также мы можем создавать несколько декораторов для одной функции, которые будут применяться последовательно:

@debug_decorator
@time_decorator
def sum(*nums):
    result = 0
    for num in nums:
        result += num
    return result

В этом примере сначала применится декоратор времени выполнения, а затем отладочный декоратор.

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

Что если мы хотели бы вызывать обернутую функцию с разными настройками? Для этого нам понадобятся декораторы с параметрами.

Общий синтаксис декоратора с параметрами выглядит так:

def decorator_with_params(param1, param2, ...):
    def actual_decorator(func):
        def wrapper(*args, **kwargs):
            # Здесь можем использовать param1, param2, ...
            result = func(*args, **kwargs)
            return result
        return wrapper
    return actual_decorator

К обычному декоратору мы добавляем еще один слой с параметрами. И, конечно, в качестве параметров мы можем передавать другие функции:

def filter_by(key):
    def inner(func):
        def wrapper(*args):
            args = key(args)
            print('filtered args = ', args)
            return func(*args)
        return wrapper
    return inner

Декоратор filter_by() принимает функцию фильтрации key(), затем применяет ее к аргументам обернутой функции, и вызывает обернутую функцию с новыми аргументами.

def odd(numbers):
    result = []
    for num in numbers:
        if num % 2 == 1:
            result.append(num)
    return result

@filter_by(odd)
def sum(*nums):
    result = 0
    for num in nums:
        result += num
    return result

sum(3, 4, 5, 6) # 8
# => filtered args =  [3, 5]

И даже функции с собственными параметрами, используя замыкания:

def less_than(max):
    def inner(numbers):
        result = []
        for num in numbers:
            if num < max:
                result.append(num)
        return result
    return inner

@filter_by(less_than(5))
def sum(*nums):
    result = 0
    for num in nums:
        result += num
    return result

sum(3, 4, 5, 6) # 7
# => filtered args =  [3, 4]

@filter_by(less_than(3))
def sum(*nums):
    result = 0
    for num in nums:
        result += num
    return result

sum(3, 4, 5, 6) # 0
# => filtered args =  []

Выводы

Мы изучили декораторы, мощный механизм для расширения функций. Существует большое количество готовых декораторов, доступных в стандартной библиотеке Python и других библиотеках. Некоторые из них позволяют кэшировать результаты функций, обеспечивать авторизацию и безопасность, профилировать код, проверять типы данных и многое другое.

Декораторы могут повлиять на производительность кода, поэтому их следует использовать с умом и осторожностью. Также нужно учитывать, что декораторы могут усложнить отладку кода и сделать его менее понятным для других разработчиков.

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


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

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

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

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

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

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

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

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

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

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

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы
профессия
Обучитесь разработке бэкенда сайтов и веб-приложений — серверной части, которая отвечает за логику и базы данных
10 месяцев
с нуля
Старт 21 ноября

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

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

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

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