Декораторы — одна из самых классных особенностей 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'>> функция {func.__name__} время выполнения (ms): {elapsed}')
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-запросы.
app = {}
def callback(route):
def wrapper(func):
app[route] = func
def wrapped():
ret = func()
return ret
return wrapped
return wrapper
@callback('/')
def index():
print('index')
return 'OK'
print('> старт')
route = app.get('/')
if route:
resp = route()
print('ответ:', resp)
print('> конец')
Проверка состояний
Декораторы можно использовать для проверки состояния перед выполнение функции: например, зарегистрирован ли пользователь, есть ли у него достаточное количество прав или валидны ли аргументы (типы, значения и так далее).
user_permissions = ["user"]
def check_permission(permission):
def wrapper_permission(func):
def wrapped_check():
if permission not in user_permissions:
raise ValueError("Недостаточно прав")
return func()
return wrapped_check
return wrapper_permission
@check_permission("user")
def check_value():
return "значение"
@check_permission("admin")
def do_something():
return "только админ"
print('старт программы')
check_value()
do_something()
print('конец программы')
Вывод:
Traceback (most recent call last):
File "C:\Programs\Python\Python38-32\test.py", line 22, in <module>
do_something()
File "C:\Programs\Python\Python38-32\test.py", line 7, in wrapped_check
raise ValueError("Недостаточно прав")
ValueError: Недостаточно прав
Создание singleton
Декоратор можно использовать для декорирования класса. Отличие лишь в том, что декоратор получает класс, а не функцию.
Singleton — это класс с одним экземпляром. Его можно сохранить как атрибут функции-обертки и вернуть при запросе. Это полезно в тех случаях, когда, например, ведется работа с соединением с базой данных.
def singleton(cls):
'''Класс Singleton (один экземпляр)'''
def wrapper_singleton(*args, **kwargs):
if not wrapper_singleton.instance:
wrapper_singleton.instance = cls(*args, **kwargs)
return wrapper_singleton.instance
wrapper_singleton.instance = None
return wrapper_singleton
@singleton
class TheOne:
pass
print('старт')
first_one = TheOne()
second_one = TheOne()
print(id(first_one))
print(id(second_one))
print('конец')
Вывод:
старт
56909912
56909912
конец
Обработка ошибок
Можно убедиться, что обрабатываются определенные типы ошибок без использования блока try
для каждой функции. Результат после этого добавляется в лог или останавливает выполнение программы.
def error_handler(func):
def wrapper(*args, **kwargs):
ret = 0
try:
ret = func(*args, **kwargs)
except:
print('>> Ошибка в функции', func.__name__)
return ret
return wrapper
@error_handler
def div(a, b):
return a / b
print('старт')
print(div(10, 2))
print(div(10, 0))
print('конец')
Вывод:
старт
5.0
>> Ошибка в функции div
0
конец
Выводы
В этом материале была разобрана тема декораторов в Python. Это удобный инструмент, который улучшает код тем, что делает его проще и гибче.
Вот некоторые из особенностей декораторов:
- Их можно использовать повторно.
- Они могут получать параметры и возвращать значения.
- Могут хранить значения/
- Могут декорировать классы.
- Они могут добавлять функциональность в другие функции или классы.
В Python есть встроенные декораторы: classmethod
, property
и staticmethod
.
Декораторы можно использовать и с другими методами (например, «магическими»), чтобы расширить возможности классов и всего проекта.