Руководство по декораторам Python
Декораторы — одна из самых классных особенностей Python. Декоратор принимает функцию, добавляет новые возможности и возвращает улучшенный вариант. Разберемся с тем, как это работает.
В Python все является объектом. И функции — не исключение. Поскольку они тоже являются объектами, их можно передавать в качестве аргументов другим функциям и возвращать в качестве результата. У них также есть атрибуты: __name__ и __doc__ .
Функция — это группа инструкций, которые выполняют определенную задачу. Задача функции — организовать код, избежать повторений и заново использовать отдельные блоки.
def function_name(args): '''docstring''' statements(s)
Синтаксис функций очень простой. Она начинается с ключевого слова def . Дальше идет уникальное имя, параметры и двоеточие. Также может быть документация (docstring), которая описывает назначение функции.
Все инструкции, которые идут дальше, составляют тело функции. Они должны иметь корректные отступы. В конце также может быть инструкция return .
def basic_function(a):
'''Базовая функция'''
print('Базовая функция:', a)
return a + 1
print('старт программы')
print('имя:', basic_function.__name__)
print('док:', basic_function.__doc__)
b = basic_function(1)
print('конец программы:', b)старт программы имя: basic_function док: Базовая функция Базовая функция: 1 конец программы: 2
Функция — это вызываемый объект. Как можно догадаться, это подразумевает объект, который можно вызвать. Проверить эту особенность можно с помощью встроенной функции callable() .
Декораторы
Итак, декоратор — это функция, которая принимает функцию, делает что-то и возвращает другую функцию.
def decorator(func):
def wrapper():
print('функция-оболочка')
func()
return wrapper
def basic():
print('основная функция')
wrapped = decorator(basic)
print('старт программы')
basic()
wrapped()
print('конец программы')Если запустить эту программу:
старт программы основная функция функция-оболочка основная функция конец программы
Разберемся с тем, что здесь произошло. Функция decorator — это, как можно понять по названию, декоратор. Она принимает в качестве параметра функцию func . Крайне оригинальные имена. Внутри функции объявляется другая под названием wrapper . Объявлять ее внутри необязательно, но так проще работать.
В этом примере функция wrapper просто вызывает оригинальную функцию, которая была передана в декоратор в качестве аргумента, но это может быть любая другая функциональность.
В конце возвращается функция wrapper . Напомним, что нам все еще нужен вызываемый объект. Теперь результат можно вызывать с оригинальным набором возможностей, а также новым включенным кодом.
Но в Python есть синтаксис для упрощения такого объявления. Чтобы декорировать функции, используется символ @ рядом с именем декоратора. Он размещается над функцией, которую требуется декорировать.
def decorator(func):
'''Основная функция'''
print('декоратор')
def wrapper():
print('-- до функции', func.__name__)
func()
print('-- после функции', func.__name__)
return wrapper @decorator
def wrapped():
print('--- обернутая функция') print('- старт программы. ')
wrapped()
print('- конец программы')декоратор - старт программы. -- до функции wrapped --- обернутая функция -- после функции wrapped - конец программы
Можно использовать тот же декоратор с любым количеством функций, а также — декорировать функцию любым декоратором.
def decorator_1(func):
print('декоратор 1')
def wrapper():
print('перед функцией')
func()
return wrapper
def decorator_2(func):
print('декоратор 2')
def wrapper():
print('перед функцией')
func()
return wrapper
@decorator_1
@decorator_2
def basic_1():
print('basic_1')
@decorator_1
def basic_2():
print('basic_2')
print('>> старт')
basic_1()
basic_2()
print('>> конец')декоратор 2 декоратор 1 декоратор 1 >> старт перед функцией перед функцией basic_1 перед функцией basic_2 >> конец
Когда у функции несколько декораторов, они вызываются в обратном порядке относительно того, как были вызваны. То есть, такой вызов:
@decorator_1 @decorator_2 def wrapped():
a = decorator_1(decorator_2(wrapped))
Если декорированная функция возвращает значение, и его нужно сохранить, то нужно сделать так, чтобы его возвращала и функция-обертка.
Декоратор-класс
Добавив метод __call__ в класс, его можно превратить в вызываемый объект. А поскольку декоратор — это всего лишь функция, то есть, вызываемый объект, класс можно превратить в декоратор с помощью функции __call__ .
Лучше всего разобрать это на примере.
class Decorator:
def __init__(self, func):
print('> Класс Decorator метод __init__')
self.func = func
def __call__(self):
print('> перед вызовом класса. ', self.func.__name__)
self.func()
print('> после вызова класса')
@Decorator
def wrapped():
print('функция wrapped')
print('>> старт')
wrapped()
print('>> конец')> Класс Decorator метод __init__ >> старт > перед вызовом класса. wrapped функция wrapped > после вызова класса >> конец
Отличие в том, что класс инициализируется при объявлении. Он должен получить функцию в качестве аргумента для метода __init__ . Это и будет декорируемая функция.
При вызове декорируемой функции на самом деле вызывается экземпляр класса. А поскольку объект вызываемый, то вызывается функция __call__ .
Функция с аргументами
А что если функция, которую требуется декорировать, должна получать аргументы? Для этого нужно вернуть функцию с той же сигнатурой, что и у декорируемой.
def decorator_with_args(func):
print('> декоратор с аргументами. ')
def decorated(a, b):
print('до вызова функции', func.__name__)
ret = func(a, b)
print('после вызова функции', func.__name__)
return ret
return decorated
@decorator_with_args
def add(a, b):
print('функция 1')
return a + b
@decorator_with_args
def sub(a, b):
print('функция 2')
return a - b
print('>> старт')
r = add(10, 5)
print('r:', r)
g = sub(10, 5)
print('g:', g)
print('>> конец')> декоратор с аргументами. > декоратор с аргументами. >> старт до вызова функции add функция 1 после вызова функции add r: 15 до вызова функции sub функция 2 после вызова функции sub g: 5 >> конец
А в случае с классом? Тот же принцип. Нужно лишь добавить желаемую сигнатуру в функцию __call__ .
class Decorator:
def __init__(self, func):
print('> Класс Decorator метод __init__')
self.func = func
def __call__(self, a, b):
print('> до вызова из класса. ', self.func.__name__)
self.func(a, b)
print('> после вызова из класса')
@Decorator
def wrapped(a, b):
print('функция wrapped:', a, b)
print('>> старт')
wrapped(10, 20)
print('>> конец')> Класс Decorator метод __init__ >> старт > до вызова из класса. wrapped функция wrapped: 10 20 > после вызова из класса >> конец
Можно использовать *args и **kwargs и для функции wrapper, если сигнатура заранее неизвестна, или будут приниматься разные типы функций.
Декораторы с аргументами
В декоратор можно передать и сам параметр. В этом случае нужно добавить еще один слой абстракции, то есть — еще одну функцию-обертку.
Это обязательно, поскольку аргумент передается декоратору. Затем функция, которая вернулась, используется для декорации нужной. Проще разобраться на примере.
def decorator_with_args(name):
print('> decorator_with_args:', name)
def real_decorator(func):
print('>> сам декоратор', func.__name__)
def decorated(*args, **kwargs):
print('>>> перед функцие', func.__name__)
ret = func(*args, **kwargs)
print('>>> после функции', func.__name__)
return ret
return decorated
return real_decorator
@decorator_with_args('test')
def add(a, b):
print('>>>> функция add')
return a + b
print('старт программы')
r = add(10, 10)
print(r)
print('конец программы')> decorator_with_args: test >> сам декоратор add старт программы >>> перед функцие add >>>> функция add >>> после функции add 20 конец программы
В декораторах-классах выполняются такие же настройки. Теперь конструктор класса получает все аргументы декоратора. Метод __call__ должен возвращать функцию-обертку, которая, по сути, будет выполнять декорируемую функцию. Например:
class DecoratorArgs:
def __init__(self, name):
print('> Декоратор с аргументами __init__:', name)
self.name = name
def __call__(self, func):
def wrapper(a, b):
print('>>> до обернутой функции')
func(a, b)
print('>>> после обернутой функции')
return wrapper
@DecoratorArgs("teste")
def add(a, b):
print('функция add:', a, b)
print('>> старт')
add(10, 20)
print('>> конец')> Декоратор с аргументами __init__: teste >> старт >>> до обернутой функции функция add: 10 20 >>> после обернутой функции >> конец
Документация
Один из атрибутов функции — строка документации (docstring), доступ к которой можно получить с помощью __doc__ . Это строковая константа, определяемая как первая инструкция в объявлении функции.
При декорации возвращается новая функция с другими атрибутами. Но они не изменяются.
def decorator(func):
'''Декоратор'''
def decorated():
'''Функция Decorated'''
func()
return decorated
@decorator
def wrapped():
'''Оборачиваемая функция'''
print('функция wrapped')
print('старт программы. ')
print(wrapped.__name__)
print(wrapped.__doc__)
print('конец программы')В этом примере функция wrapped — это, по сути, функция decorated , которую она заменяет.
старт программы. decorated Функция Decorated конец программы
Вот где на помощь приходит функция wraps из модуля functools . Она сохраняет атрибуты оригинальной функции. Нужно лишь декорировать функцию wrapper с ее помощью.
from functools import wraps
def decorator(func):
'''Декоратор'''
@wraps(func)
def decorated():
'''Функция Decorated'''
func()
return decorated
@decorator
def wrapped():
'''Оборачиваемая функция'''
print('функция wrapped')
print('старт программы. ')
print(wrapped.__name__)
print(wrapped.__doc__)
print('конец программы')старт программы. wrapped Оборачиваемая функция конец программы
Приложения
До этого момента мы не касались того, как декораторы используются в реальных приложения. Поэтому перейдем к примерам.
Таймеры
Базовая функциональность — время работы функции. Есть возможность получить время до и после вызова функции, использовав полученный результат (для записи в лог, базу данных, для отладки и так далее).
from datetime import datetime
import time
def elapsed(func):
def wrapper(a, b, delay=0):
start = datetime.now()
func(a, b, delay)
end = datetime.now()
elapsed = (end - start).total_seconds() * 1000
print(f'>> функция время выполнения (ms): ')
return wrapper
@elapsed
def add_with_delay(a, b, delay=0):
print('сложить', a, b, delay)
time.sleep(delay)
return a + b
print('старт программы')
add_with_delay(10, 20)
add_with_delay(10, 20, 1)
print('конец программы')старт программы сложить 10 20 0 >> функция add_with_delay время выполнения (ms): 36.006 сложить 10 20 1 >> функция add_with_delay время выполнения (ms): 1031.255 конец программы
Логи
Еще один распространенный сценарий применения для декоратора — логирование функций.
import logging
def logger(func):
log = logging.getLogger(__name__)
def wrapper(a, b):
log.info("Вызов функции ", func.__name__)
ret = func(a, b)
log.info("Вызвана функция ", func.__name__)
return ret
return wrapper
@logger
def add(a, b):
print('a + b:', a + b)
return a + b
print('>> старт')
add(10, 20)
add(20, 30)
print('>> конец')Функция обратного вызова
Функция обратного вызова — это функция, которая вызывается при срабатывании определенного события (переходе на страницу, получении сообщения или окончании обработки процессором).
Можно передать функцию, чтобы она выполнилась после определенного события. Это используется, например, в HTTP-серверах в ответ на URL-запросы.