В программировании часто возникают задачи, когда нужно добавить поведение к уже существующим функциям или классам. Например, логирование, проверку входных данных или замер времени выполнения функции. В таких случаях использование декораторов может значительно упростить решение задачи.
В этом уроке мы рассмотрим, что такое декораторы и как их использовать, чтобы добавлять дополнительную функциональность к существующим функциям.
Что такое декораторы
Ранее мы рассматривали как можно "запомнить" передаваемые значения в функцию с помощью замыканий. Напомним как выглядит синтаксис:
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 и других библиотеках. Некоторые из них позволяют кэшировать результаты функций, обеспечивать авторизацию и безопасность, профилировать код, проверять типы данных и многое другое.
Декораторы могут повлиять на производительность кода, поэтому их следует использовать с умом и осторожностью. Также нужно учитывать, что декораторы могут усложнить отладку кода и сделать его менее понятным для других разработчиков.
При этом декораторы являются сильным инструментом, который позволяет легко добавлять дополнительную функциональность к существующим функциям и классам и не изменять их исходный код.
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
- Статья «Как учиться и справляться с негативными мыслями»
- Статья «Ловушки обучения»
- Статья «Сложные простые задачи по программированию»
- Вебинар «Как самостоятельно учиться»
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.