Декораторы в Python, часть 2

Oleksandr Shepetko18 ноября, 07:00 20 0

Несколько примеров использования Python декораторов в реальной жизни.

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

import functools

def decorator(func):
    @functools.wraps(func)
    def wrapper_decorator(*args, **kwargs):
        # Do something before
        value = func(*args, **kwargs)
        # Do something after
        return value
    return wrapper_decorator

Этот шаблон и будем использовать в дальнейшем в качестве начального при разработке любых декораторов.

Измерение времени выполнения функции

Итак, давайте продолжим наше путешествие в мир декораторов и начнём с рассмотрения декоратора, который измеряет время выполнения функции.

import functools
import time

def timer(func):
    """Print the runtime of the decorated function"""
    @functools.wraps(func)
    def wrapper_timer(*args, **kwargs):
        start_time = time.perf_counter()    # 1
        value = func(*args, **kwargs)
        end_time = time.perf_counter()      # 2
        run_time = end_time - start_time    # 3
        print(f"Finished {func.__name__!r} in {run_time:.4f} secs")
        return value
    return wrapper_timer

@timer
def waste_some_time(num_times):
    for _ in range(num_times):
        sum([i**2 for i in range(10000)])

Как видите, никакой особой сложности собой декоратор не представляет. Перед запуском декорируемой функции на выполнение, в строке #1,  декоратор запоминает текущее значение таймера. После того, как декорируемая функция завершит работу, в строках #2 и #3 вычисляется время, затраченное на выполнение функции, и выводится в консоль.

>>> waste_some_time(1)
Finished 'waste_some_time' in 0.0010 secs

>>> waste_some_time(999)
Finished 'waste_some_time' in 0.3260 secs

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

Отладка кода

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

import functools

def debug(func):
    """Print the function signature and return value"""
    @functools.wraps(func)
    def wrapper_debug(*args, **kwargs):
        args_repr = [repr(a) for a in args]                      # 1
        kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]  # 2
        signature = ", ".join(args_repr + kwargs_repr)           # 3
        print(f"Calling {func.__name__}({signature})")
        value = func(*args, **kwargs)
        print(f"{func.__name__!r} returned {value!r}")           # 4
        return value
    return wrapper_debug

Давайте подробнее разберём, что здесь происходит:

  1. Создаётся список полученных аргументов функции, каждый из который пропускается через repr() с целью получения удобоваримого строкового представления значения аргумента.
  2. Создаётся список keyword-аргументов. Обратите внимание на использование f-синтаксиса, где v!r интерпретируется при помощи repr().
  3. Оба предыдущих списка аргументов склеиваются в одну сигнатуру, которая печатается в следующей строке.
  4. Печатается результат, возвращаемый декорируемой функцией.

На реальном примере работа этого декоратора будет выглядеть примерно так:

@debug
def make_greeting(name, age=None):
    if age is None:
        return f"Howdy {name}!"
    else:
        return f"Whoa {name}! {age} already, you are growing up!"
>>> make_greeting("Benjamin")
Calling make_greeting('Benjamin')
'make_greeting' returned 'Howdy Benjamin!'
'Howdy Benjamin!'

>>> make_greeting("Richard", age=112)
Calling make_greeting('Richard', age=112)
'make_greeting' returned 'Whoa Richard! 112 already, you are growing up!'
'Whoa Richard! 112 already, you are growing up!'

>>> make_greeting(name="Dorrisile", age=116)
Calling make_greeting(name='Dorrisile', age=116)
'make_greeting' returned 'Whoa Dorrisile! 116 already, you are growing up!'
'Whoa Dorrisile! 116 already, you are growing up!'

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

В следующем примере представлен код, вычисляющий значение математической константы e:

import math
from decorators import debug

# Apply a decorator to a standard library function
math.factorial = debug(math.factorial)

def approximate_e(terms=18):
    return sum(1 / math.factorial(n) for n in range(terms))

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

>>> approximate_e(5)
Calling factorial(0)
'factorial' returned 1
Calling factorial(1)
'factorial' returned 1
Calling factorial(2)
'factorial' returned 2
Calling factorial(3)
'factorial' returned 6
Calling factorial(4)
'factorial' returned 24
2.708333333333333

Замедление выполнения кода

Следующий пример на первый взгляд может показаться совершенно бесполезным. Действительно, зачем может кому-то понадобиться намеренно заставлять работать программу медленнее? Возможно, самый распространённый случай -- это необходимость ограничить частоту выполнения функции, которая, например, периодически опрашивает какой-то ресурс. Это может быть, например, проверка того, не изменилось ли содержимое какой-либо веб-страницы. Представленный ниже декоратор @slow_down выполняет задержку в одну секунду, прежде чем вызвать декорируемую функцию:

import functools
import time

def slow_down(func):
    """Sleep 1 second before calling the function"""
    @functools.wraps(func)
    def wrapper_slow_down(*args, **kwargs):
        time.sleep(1)
        return func(*args, **kwargs)
    return wrapper_slow_down

@slow_down
def countdown(from_number):
    if from_number < 1:
        print("Liftoff!")
    else:
        print(from_number)
        countdown(from_number - 1)
>>> countdown(3)
3
2
1
Liftoff!

Также обратите внимание, что функция countdown является рекурсивной.

Регистрация плагин

Очевидно, что декораторы не обязаны в обязательном порядке изменять поведение декорируемой функции, то есть декорировать её. Декоратор вправе вернуть объект исходной функции, ничего с ней не делая. Такой паттерн использования декораторов может оказаться полезным при реализации легковесного механизма регистрации расширений вашего приложения.

import random
PLUGINS = dict()

def register(func):
    """Register a function as a plug-in"""
    PLUGINS[func.__name__] = func
    return func

@register
def say_hello(name):
    return f"Hello {name}"

@register
def be_awesome(name):
    return f"Yo {name}, together we are the awesomest!"

def randomly_greet(name):
    greeter, greeter_func = random.choice(list(PLUGINS.items()))
    print(f"Using {greeter!r}")
    return greeter_func(name)

В приведённом примере декоратор @register просто сохраняет ссылку на полученную функцию в словаре PLUGINS. Обратите внимание, что здесь не использовался @functools.wrap, поскольку в данном случае в нём просто нет смысла, ведь из декоратора происходит возврат оригинальной функции.

Функция randomly_greet() наглядно демонстрирует работу декоратора @register:

>>> PLUGINS
{'say_hello': <function say_hello at 0x7f768eae6730>,
 'be_awesome': <function be_awesome at 0x7f768eae67b8>}

>>> randomly_greet("Alice")
Using 'say_hello'
'Hello Alice'

Основной плюс приведённого в примере подхода заключается в его крайней простоте за счёт использования декоратора вместо более громоздких конструкций.

Проверка авторизации пользователя

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

from flask import Flask, g, request, redirect, url_for
import functools
app = Flask(__name__)

def login_required(func):
    """Make sure user is logged in before proceeding"""
    @functools.wraps(func)
    def wrapper_login_required(*args, **kwargs):
        if g.user is None:
            return redirect(url_for("login", next=request.url))
        return func(*args, **kwargs)
    return wrapper_login_required

@app.route("/secret")
@login_required
def secret():
    ...