Руководство по декораторам Python

Декораторы — одна из самых классных особенностей Python. Декоратор принимает функцию, добавляет новые возможности и возвращает улучшенный вариант. Разберемся с тем, как это работает.

  1. Введение

В 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.

Декораторы можно использовать и с другими методами (например, «магическими»), чтобы расширить возможности классов и всего проекта.

Максим
Я создал этот блог в 2018 году, чтобы распространять полезные учебные материалы, документации и уроки на русском. На сайте опубликовано множество статей по основам python и библиотекам, уроков для начинающих и примеров написания программ.
Мои контакты: Почта
admin@pythonru.comAlex Zabrodin2018-10-26OnlinePython, Programming, HTML, CSS, JavaScript