Функции можно определять внутри других функций, а можно передать функцию как аргумент в другую функцию — и вызвать её там.
Например, если во время тестирования проекта понадобится измерить время работы нескольких функций, — будет рациональнее один раз написать функцию, проверяющую быстродействие, и предавать в неё тестируемые функции.
Чтобы измерить время выполнения функций sleep_one_sec()
и sleep_two_sec()
, нужно написать функцию, которая будет вычислять и печатать время выполнения этих функций.
import time
def sleep_one_sec():
print('Перерыв 1 секунда')
time.sleep(1)
return 'Возвращаемое значение'
def sleep_two_sec():
time.sleep(2)
# Функция для измерения быстродействия
# принимает на вход тестируемую функцию
def time_of_function(func):
def wrapper():
# Засекаем время перед выполнением тестируемой функции
start_time = time.time()
# Вызываем тестируемую функцию и
# cохраняем результат выполнения в переменную
result = func()
# Вычисляем, округляем и печатаем разницу
# между временем старта и актуальным временем
execution_time = round(time.time() - start_time, 1)
print(f'Время выполнения функции: {execution_time} с.')
# Возвращаем результат выполнения тестируемой функции
# Если этого не сделать, результат нельзя будет использовать
# в дальнейшем коде
return result
return wrapper
# Передаём функцию sleep_one_sec() в time_of_function()
measured_sleep_one_sec = time_of_function(sleep_one_sec)
print(measured_sleep_one_sec())
# Будет напечатано:
# Перерыв 1 секунда
# Время выполнения функции: 1.0 с.
# Возвращаемое значение
# Передаём функцию sleep_two_sec() в time_of_function()
measured_sleep_two_sec = time_of_function(sleep_two_sec)
measured_sleep_two_sec()
# Будет напечатано: Время выполнения функции: 2.0 с.
time_of_function()
— это и есть декоратор. Обёртка-замыкание изменяет поведение декорируемой функции. Сама же декорируемая функция при этом не модифицируется.
Для работы с декораторами в Python добавлен синтаксический сахар (англ. syntactic sugar), упрощённый синтаксис.
import time
# Функция для измерения быстродействия функции-аргумента
def time_of_function(func):
def wrapper():
start_time = time.time()
result = func()
execution_time = round(time.time() - start_time, 1)
print(f'Время выполнения функции: {execution_time} с.')
return result
return wrapper
def sleep_one_sec():
time.sleep(1)
# Декорироваие без синтаксического сахара:
sleep_one_sec = time_of_function(sleep_one_sec)
# То же самое декорирование с применением синтаксического сахара:
# имя функции-декоратора (с символом @)
# ставится перед объявлением декорируемой функции
@time_of_function
def sleep_one_sec():
time.sleep(1)
# После декорирования любой вызов функции sleep_one_sec()
# будет автоматически сопровождаться измерением времени её выполнения
sleep_one_sec()
# Будет напечатано: Время выполнения функции: 1.0 с.
# Когда необходимость в замерах отпадёт — декоратор можно убрать
Декораторы могут менять не только поведение декорируемой функции, но и значение, возвращаемое этой функцией:
def uppercase(func):
def wrapper():
original_result = func()
return f'Большие {original_result.upper()}'
return wrapper
@uppercase
def greet():
return 'маленькие буквы'
print(greet())
# Будет напечатано: Большие МАЛЕНЬКИЕ БУКВЫ
Специальные атрибуты Python
Функции в Python обладают набором специальных атрибутов, через которые доступна «служебная информация». Описание атрибутов есть в документации.
С их помощью можно, например, получить имя функции (__name__
) или содержимое docstring — строковой переменной, в которой кратко описан объект (__doc__
):
def what_my_name():
"""Это docstring функции what_my_name()."""
...
print(f'Имя функции: {what_my_name.__name__}')
# Будет напечатано: Имя функции: what_my_name
print(f'Докстринг функции: {what_my_name.__doc__}')
# Будет напечатано: Докстринг функции: Это docstring функции what_my_name().
Примечание: если вам любопытно, что ещё хранит в себе функция, то вывести список доступных свойств и методов любого объекта в Python можно так:
def what_my_name():
"""Это docstring функции what_my_name()."""
...
print(dir(what_my_name))
Подводные камни name и doc
Одну функцию можно обернуть одновременно несколькими декораторами.В примере ниже декораторы first_decorator()
и second_decorator(func)
распечатывают имя и докстринг декорируемой функции.Функция do_nothing()
декорирована дважды, чтобы дважды распечатать её имя и докстринг:
def first_decorator(func):
def wrapper1():
"""Это декоратор first_decorator."""
print(f'Докстринг декорируемой функции: {func.__doc__}')
print(f'Декорируется функция {func.__name__}')
return func()
return wrapper1
def second_decorator(func):
def wrapper2():
"""Это декоратор second_decorator."""
print(f'Докстринг декорируемой функции: {func.__doc__}')
print(f'Декорируется функция {func.__name__}')
return func()
return wrapper2
@first_decorator
@second_decorator
def do_nothing():
"""Я ничего не знаю. Я никуда не летаю."""
...
do_nothing()
# Ожидаем, что будет дважды выведен один и тот же текст:
# Докстринг декорируемой функции: Я ничего не знаю. Я никуда не летаю.
# Декорируется функция do_nothing
# Докстринг декорируемой функции: Я ничего не знаю. Я никуда не летаю.
# Декорируется функция do_nothing
# Но это (неожиданно) не работает.
# Будет напечатано:
# Докстринг декорируемой функции: Это декоратор second_decorator.
# Декорируется функция wrapper2
# Докстринг декорируемой функции: Я ничего не знаю. Я никуда не летаю.
# Декорируется функция do_nothing
Сперва были напечатаны __name__
и __doc__
из second_decorator
, а не из декорируемой функции! Что-то пошло не так.
Если отказаться от синтаксического сахара, то при двойном декорировании получается такая конструкция:
def do_nothing():
"""Я ничего не знаю. Я никуда не летаю."""
...
do_nothing = first_decorator(second_decorator(do_nothing))
do_nothing()
Сначала first_decorator
печатает __name__
и __doc__
из second_decorator
(а это не то, что требовалось), а после этого second_decorator
печатает специальные атрибуты из декорируемой функции.
Для решения этой проблемы авторы стандартной библиотеки functools
написали декоратор @wraps
.
Почитайте, как работает этот декоратор. В коде ниже он использован правильно:
from functools import wraps
def first_decorator(func):
@wraps(func) # Задекорировали обёртку
def wrapper1():
"""Это декоратор first_decorator."""
print(f'Докстринг декорируемой функции: {func.__doc__}')
print(f'Декорируется функция {func.__name__}')
return func()
return wrapper1
def second_decorator(func):
@wraps(func) # И здесь задекорировали обёртку
def wrapper2():
"""Это декоратор second_decorator."""
print(f'Докстринг декорируемой функции: {func.__doc__}')
print(f'Декорируется функция {func.__name__}')
return func()
return wrapper2
@first_decorator
@second_decorator
def do_nothing():
"""Я ничего не знаю. Я никуда не летаю."""
...
do_nothing()
Запустите этот код и убедитесь: теперь всё работает как надо. Конструкции декораторов с @wraps
часто встречаются на практике, вам это пригодится.
Работа с аргументами
Чтобы декоратор был универсальным — он должен принимать на вход функции с любым количеством и типом параметров.
Для этого в Python есть конструкции *args
и **kwargs
.
def any_func(*args, **kwargs):
...
Этот синтаксис означает, что функция готова принять любое количество позиционных (*args
от arguments) и именованных (**kwargs
от keyword arguments) аргументов.С переменной *args
можно работать как с кортежем, а с переменной **kwargs
— как со словарём.Такой синтаксис может применяться в любых функциях (не только в декораторах). Имена args и kwargs не предустановлены, но общеприняты.
Декоратор @authorized_only
Самое время решить задачу по ограничению прав доступа к определённым страницам сайта: изменить несколько view-функций так, чтобы они выполнялись, только если пользователь авторизован.Решение понятно: написать декоратор, проверяющий авторизацию, и обернуть им нужные view-функции.
from django.shortcuts import redirect
...
def authorized_only(func):
# Функция-обёртка в декораторе может быть названа как угодно
def check_user(request, *args, **kwargs):
# В любую view-функции первым аргументом передаётся объект request,
# в котором есть булева переменная is_authenticated,
# определяющая, авторизован ли пользователь.
if request.user.is_authenticated:
# Возвращает view-функцию, если пользователь авторизован.
return func(request, *args, **kwargs)
# Если пользователь не авторизован — отправим его на страницу логина.
return redirect('/auth/login/')
return check_user
# Декорируем view-функцию
@authorized_only
def some_view(request):
# Доступно только авторизованным!
...