Декораторы в Python

Функции можно определять внутри других функций, а можно передать функцию как аргумент в другую функцию — и вызвать её там.

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

Чтобы измерить время выполнения функций 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):
    # Доступно только авторизованным!
    ...
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: