Декораторы в Python — это паттерн проектирования, предназначенный для расширения функциональности объектов без вмешательства в их код.
Запись декоратора, представленная через символ @
перед названием изменяемой функции, по сути является синтаксическим сахаром в питоне.
Для того чтобы разобраться как работают декораторы, нужно понять как устроены области видимости и замыкания.
В Python существует 4 области видимости, но обычно говорят про три области видимости, которые идут первыми в нижеприведенном списке.
LEGB области видимости:
global_variable = 1
def outer():
enclosing_variable = 2
def inner():
local_variable = 3
print("local", local_variable)
print("enclosing", enclosing_variable)
print("global", global_variable)
print("build-in", __name__)
inner()
outer()
# Терминал выдаст следующее:
# local 3
# enclosing 2
# global 1
# build-in __main__
inner()
и доступна только из нее; __name__
получила значение из встроенной области видимости. Это специальная область видимости Python, которая содержит имена встроенные в Python. Имена в этой области Python также доступны повсюду в вашем коде.Функция может использовать переменную, объявленную во внешней области видимости, даже если эта переменная явно не передана в функцию. Обратиться к локальной переменной из глобальной области видимости не получится.
Аналогично, если мы попытаемся из глобальной области видимости вызвать функцию inner()
, то получим сообщение об ошибке.
NameError: name 'inner' is not defined.
Перед тем как дать определение замыканию, давай посмотрим несколько примеров.
Вложенную функцию можно не только вызвать в объемлющей функции, как это было в предыдущем примере, но и вернуть как результат выполнения. Данный результат можно будет сохранить в какой-нибудь переменной.
Немного модифицирую пример, которые был ранее. Функция делает все то же самое, только убрал переменную, которая обращается ко встроенной области видимости и вместо «принтов» сделали возврат значений через оператор return
.
global_variable = 1
def outer():
enclosing_variable = 2
def inner():
local_variable = 3
return (
f"local -> {local_variable}, "
f"enclosing -> {enclosing_variable}, "
f"global -> {global_variable}"
)
return inner()
Результат работы данной функции присвоим переменной и выведем в консоль:
test_func = outer()
print(test_func)
# Выведет в консоль:
# local -> 3, enclosing -> 2, global -> 1
А что будет если удалить функцию? Удалять конечно мы будем не стирая код, а через оператор del
, для наглядности покажу весь код.
global_variable = 1
def outer():
enclosing_variable = 2
def inner():
local_variable = 3
return (
f"local -> {local_variable}, "
f"enclosing -> {enclosing_variable}, "
f"global -> {global_variable}"
)
return inner()
test_func = outer()
print(test_func)
del outer # Что произойдет после того как мы удалили функцию?
print(test_func)
# Вывод в консоль после запуска скрипта
# local -> 3, enclosing -> 2, global -> 1
# local -> 3, enclosing -> 2, global -> 1
Функция outer()
— это своеобразная фабрика для создания и настройки копий функции inner()
.
После удаления outer()
вся её контекстная область видимости будет сохранена, потому что на эту область видимости существуют ссылки в объекте test_func
.
Во вложенную функцию мы можем передавать в том числе аргументы.
def print_message(name):
def inner(message):
return f"{name}, {message}!"
return inner
message_to_ivan = print_message("Иван")
message_to_petr = print_message("Петр")
print(message_to_ivan("привет"))
print(message_to_ivan("пока"))
# Иван, привет!
# Иван, пока!
print(message_to_petr("привет"))
print(message_to_petr("пока"))
# Петр, привет!
# Петр, пока!
В вышеприведенном примере, после объявления функции, происходит следующее:
name
.inner()
. И тут можно сказать следующее. Замыкание (на английском «closure») — это способность вложенной функции запоминать локальное состояние контекстной области объемлющей функции.
И еще пример замыкания, внутри которого используются логические выражения.
def check_age_and_return_message(age, message):
def entry_allowed():
return f"{message} {age}? Ну ок. Вход разрешен."
def no_entry():
return f"{message} Т.к. тебе {age}, то вход запрещен"
if age < 18:
return no_entry
return entry_allowed
mike = check_age_and_return_message(18, "Сколько тебе лет?")
print(mike())
# Вывод в консоль:
# Сколько тебе лет? 18? Ну ок. Вход разрешен.
Пример очень простой и, конечно же, в реальности подобную механику можно реализовать гораздо проще и без использования замыканий.
А вот и пример декоратора, который выводит время выполнения кода функции.
import time
def time_of_function(func):
def wrapper(*args):
t0 = time.perf_counter()
result = func(*args)
elapsed = time.perf_counter() - t0
name = func.__name__
arg_str = ", ".join(repr(arg) for arg in args)
print(f"{elapsed:0.10f}s -> {name}({arg_str}) -> {result}")
return result
return wrapper
@time_of_function
def factorial(n):
return 1 if n < 2 else n * factorial(n - 1)
factorial(10)
Результат выполнения функции будет следующим:
0.0000004000s -> factorial(1) -> 1
0.0002123000s -> factorial(2) -> 2
0.0004742000s -> factorial(3) -> 6
0.0005836000s -> factorial(4) -> 24
0.0007005000s -> factorial(5) -> 120
0.0008124000s -> factorial(6) -> 720
0.0009196000s -> factorial(7) -> 5040
0.0010263000s -> factorial(8) -> 40320
0.0011475000s -> factorial(9) -> 362880
0.0012911000s -> factorial(10) -> 3628800
time_of_function()
— это и есть декоратор. Обёртка-замыкание изменяет поведение декорируемой функции. Сама же декорируемая функция при этом не модифицируется.
time_of_function()
делает следующее:
t0
.factorial
и сохраняет результат.Для работы с декораторами в Python добавлен синтаксический сахар, упрощённый синтаксис. Благодаря этому мы можем просто перед изменяемой функцией добавить @название_нашего_декоратора
.
Пример выше можно написать и без использования синтаксического сахара.
import time
def time_of_function(func):
def wrapper(*args):
t0 = time.perf_counter()
result = func(*args)
elapsed = time.perf_counter() - t0
name = func.__name__
arg_str = ", ".join(repr(arg) for arg in args)
print(f"{elapsed:0.10f}s -> {name}({arg_str}) -> {result}")
return result
return wrapper
def factorial(n):
return 1 if n < 2 else n * factorial(n - 1)
factorial_var = time_of_function(factorial)
factorial_var(10)
Разница будет лишь в том, что будет выведен конечный результат работы функции, т.е. когда вычисляется факториал из десяти. Но в обоих случаях декоратор time_of_function
получает функцию factorial
в качестве аргумента, затем он создает и возвращает внутреннюю функцию wrapper
, которую Python под капотом связывает с именем factorial
Функция с замером времени имела несколько недостатков.
Во-первых, функция принимает на вход только позиционные аргументы. Чтобы получать в том числе и именованные, то нужно добавить распаковку **kwargs
для wrapper
:
def wrapper(*args, **kwargs):
pass
Этот синтаксис означает, что функция готова принять любое количество позиционных (*args
) и именованных (**kwargs
) аргументов.
С переменной *args
можно работать как с кортежем, а с переменной **kwargs
— как со словарем.
Такой синтаксис может применяться в любых функциях, не только в декораторах. Имена args
и kwargs
не предустановлены, но общеприняты.
Во-вторых, в случае если у нас будет использоваться несколько декораторов, то атрибуты __name__
или __doc__
выведут нам неправильный результат. А для того, чтобы результат был корректным, нужно использовать декоратор из стандартной библиотеки functools — wraps
.
Для того, чтобы чуть раскрыть данную тему, напишем пару декораторов: один будет засекать время выполнения функции, второй кешировать значение декорируемой функции.
import time
from functools import wraps
def time_of_function(func):
@wraps(func)
def wrapper(*args, **kwargs):
start_time = time.perf_counter()
result = func(*args, **kwargs)
execution_time = time.perf_counter() - start_time
print(f"Время выполнения функции {func.__name__}: {execution_time:0.2f} с.")
print()
return result
return wrapper
def cache_args(func):
arguments = {}
@wraps(func)
def wrapper(*args):
result = args
if result in arguments:
return arguments[result]
arguments[result] = func(*args)
print("Передан аргумент", *args, "Результат выполнения:", arguments[result])
return arguments[result]
return wrapper
@time_of_function
@cache_args
def get_square_number(num):
time.sleep(1)
return num**2
get_square_number(10)
get_square_number(10)
get_square_number(5)
get_square_number(5)
Результат выполнения:
Передан аргумент 10 Результат выполнения: 100
Время выполнения функции get_square_number: 1.00 с.
Время выполнения функции get_square_number: 0.00 с.
Передан аргумент 5 Результат выполнения: 25
Время выполнения функции get_square_number: 1.00 с.
Время выполнения функции get_square_number: 0.00 с.**
По порядку:
time_of_function
уже нам знакома, она лишь немного видоизменилась.cache_args
отвечает за кеширование переданных аргументов. Если аргумент уже был сохранен в переменной result
, то мы просто возвращаем уже ранее сохраненное значение. Если же в result
такого аргумента нет, то мы его высчитываем и выводим дополнительно сообщение в консоль.get_square_number
вполне обычная, только здесь еще добавлен вызов time.sleep(1)
, чтобы мы могли понять была ли функция вызвана или же результат вернулся из кеша. Задержка в одну секунду прекрасно позволяет это сделать.В результате выполнения кода мы можем заметить, что функция фактически была выполнена всего два раза, хотя вызывали мы ее четыре раза.
Выше я писал о декораторе @wraps
, которому мы в данном примере передали функцию из аргумента. Именно этот декоратор позволил корректно вывести __name__
внутри декоратора.
Давай попробуем убрать декоратор @wraps
и заодно переименуем название функции wrapper
внутри cache_args
и посмотрим, что случится:
import time
def time_of_function(func):
def wrapper(*args, **kwargs):
start_time = time.perf_counter()
result = func(*args, **kwargs)
execution_time = time.perf_counter() - start_time
print(f"Время выполнения функции {func.__name__}: {execution_time:0.2f} с.")
print()
return result
return wrapper
def cache_args(func):
arguments = {}
def wrapper1(*args):
result = args
if result in arguments:
return arguments[result]
arguments[result] = func(*args)
print("Передан аргумент", *args, "Результат выполнения:", arguments[result])
return arguments[result]
return wrapper1
@time_of_function
@cache_args
def get_square_number(num):
time.sleep(1)
return num**2
get_square_number(10)
get_square_number(10)
get_square_number(5)
get_square_number(5)
Результат выполнения:
Передан аргумент 10 Результат выполнения: 100
Время выполнения функции wrapper1: 1.00 с.
Время выполнения функции wrapper1: 0.00 с.
Передан аргумент 5 Результат выполнения: 25
Время выполнения функции wrapper1: 1.00 с.
Время выполнения функции wrapper1: 0.00 с.
И что мы здесь видим? Название декорируемой функции некорректное и по факту мы видим, что передается название функции wrapper1
из функции cache_args
, т.к. именно эту обертку мы и возвращаем на вход функции time_of_function
. Но в остальном все выполнено правильно.
Декораторы в Python — очень мощный и часто используемый инструмент. Ты можешь увидеть его и при работе с Django, и с FastApi и с чем-либо еще.
Эта заметка не описывает все возможности, которые есть, но как базовое интро вполне подойдет.
Думаю наиболее часто в работе используются уже готовые декораторы из стандартной библиотеки или из библиотек фреймворков с которыми необходимо работать. Но то как работают эти инструменты очень важно.
Плюс ко всему на собеседованиях это тоже очень может пригодиться.