Python

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

Краткий конспект по книге “Мощный питон” А. Максвелл. Глава 4.

Декоратор – это функция, которая модифицирует поведение других функций путем добавления
строк кода, выполняемых перед запуском этих функций и/или после возврата из нее.
При этом код внутри функции не меняется.

Декоратор – это простая функция, принимающая в качестве аргумента другую функцию (без скобок).
Результат работы декоратора – другая функция.

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

Создание декоратора

def some_decorator(func):  # Это декоратор. Он принимает декорируемую функцию как аргумент

    # Это функция-обертка. Она выполняется всякий раз при вызове декорированной функции
    def wrapper(n):  
        return func(n) + 1

    # Декоратор возвращает функцию-обертку
    return wrapper

Основное свойство декоратора

@some_decorator  # Таким образом происходит оборачивание функции в декоратор
def some_funtion(arg):
    pass

Это выражение равносильно следующему:

def some_function(arg):
    pass
some_function = some_decorator(some_function)

То есть декорированная функция по сути является аргументом функции-декоратора.

Если пометить функцию декоратором, ее будет нельзя вызвать напрямую. Она всегда будет работать через декоратор.

Уточнение: Её можно вызвать напрямую, если обратиться к атрибуту __wrapped__ (при условии использования @wraps). Это бывает полезно при тестировании. foo.__wrapped__(1) — вызовет оригинал без декоратора.

Зачем нужен @wraps?

При создании своего кастомного декоратора полезно использовать встроенный в Python декоратор @wraps.

from functools import wraps

def some_decorator(func): 

# Необходим, чтобы сохранить __name__, __doc__, __annotations__ декорированной функции.
    @wraps(func)

    def wrapper(n):  
        """Это функция-обертка"""
        return func(n) + 1

    return wrapper

Если не добавлять @wraps, то при вызове декорированной функции и ее атрибутов мы увидим информацию о функции wrapper. Это затрудняет отладку кода.

func(1)
print(func.__name__)
print(func.__doc__)
print(func.__annotations__)

Вывод:
wrapper
Это функция-обертка
{}

Универсальный декоратор, принимающий любое число аргументов

def printlog_1(func):
    @wraps(func)
    def wrapper(*args, **kwargs):  # может принимать любое число аргументов
        print("CALLING: " + func.__name__)
        return func(*args, **kwargs)

    return wrapper

Использование nonlocal

Если необходимо сохранять счетчик или какие-либо данные в декораторе, следует использовать такую конструкцию.

def countcalls(func):
    count = 0  # Это счетчик

    @wraps(func)
    def wrapper(*args, **kwargs):

        # Ключевое слово nonlocal показывает функции wrapper, что переменная count находится на следующем уровне области видимости, но не является глобальной
        nonlocal count  
        count += 1
        print(f"# of calls: {count}")
        return func(*args, **kwargs)

    return wrapper

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

Подробный пример с сохранением данных в словаре data.

"""Некоторые задачи требуют хранить данные в функции-декораторе - стр. 77"""


def running_average(func):
    """Эта функция-декоратор умеет хранить данные многочисленных запусков
    декорированной функции в словаре data"""

    # Эта часть декоратора срабатывает только один раз - при первом вызове
    # декорированной функции. Все остальные вызовы этой функции приводят к
    # к работе только функции wrapper.
    # Именно благодаря этому словарь data накапливает информацию и работает независимо
    # для разных декорированных функций.
    data = {"total": 0, "count": 0}

    @wraps(func)  # Необходим, чтобы сохранить __name__, __doc__, __annotations__ декорированной функции.
    def wrapper(*args, **kwargs):
        val = func(*args, **kwargs)
        data["total"] += val
        data["count"] += 1

        # В реальном проекте эти данные удобнее записывать в лог, а не выводить на печать
        print("Average of {} so far: {:.01f}".format(
            func.__name__, data["total"] / data["count"]
        ))
        return val

    # Здесь словарь data присваивается функции wrapper в качестве атрибута, и теперь мы можем к нему обращаться извне.
    # Например, из декорированной функции - foo.data
    # Это один из приемов аннотирования функций - прикрепления к ним дополнительной информации.
    wrapper.data = data

    return wrapper


@running_average
def foo(x: int):
    """Добавляет 2 к аргументу"""
    return x + 2


@running_average
def buzz(x, y):
    return x ** y


foo(1)
print(foo.__name__)
print(foo.__doc__)
print(foo.__annotations__)
print(foo.data)
print()
foo(10)
print(foo.data)
foo(1)
print(foo.data)
foo(1)
print(foo.data)

buzz(2, 3)
print(buzz.data)
buzz(3, 4)
print(buzz.data)
buzz(5, 6)
print(buzz.data)

Вывод:
Здесь хорошо видно, что данные для двух разных функций накапливаются независимо.

Average of foo so far: 3.0
wrapper
None
{}
{'total': 3, 'count': 1}

Average of foo so far: 7.5
{'total': 15, 'count': 2}
Average of foo so far: 6.0
{'total': 18, 'count': 3}
Average of foo so far: 5.2
{'total': 21, 'count': 4}
Average of buzz so far: 8.0
{'total': 8, 'count': 1}
Average of buzz so far: 44.5
{'total': 89, 'count': 2}
Average of buzz so far: 5238.0
{'total': 15714, 'count': 3}

Как передать аргумент в декоратор?

"""Как передать аргумент в декоратор?
Необходимо обернуть уже имеющийся декоратор еще одной функцией,
которая принимает нужный аргумент."""


def add(increment):
    def decorator(func):
        @wraps(func)
        def wrapper(n):
            return func(n) + increment

        return wrapper

    return decorator


# add = add(20)  # Можно сначала присвоить декоратор с аргументом отдельному объекту
# @add           # И далее вызвать его перед функцией
# def foo(x):
#     return x + 10
# print(foo(5))

@add(20)  # Но можно и так
def foo(x):
    return x + 10


print(foo(5))

Вывод:
35

Как создать декоратор в виде класса?

"""Декоратор можно реализовать не только в виде функции, но и в виде класса.
Это открывает возможности для использования всех свойств ООП, включая наследование."""


class Prefixer:
    def __init__(self, prefix):
        self.prefix = prefix

    def __call__(self, message):  # Этот метод делает класс вызываемым, подобно функции, принимающей аргумент message
        return self.prefix + message


# Теперь можно вызывать экземпляры этого класса как функции
simonsays = Prefixer("Simon says: ")  # Создаем экземпляр
print(simonsays("Get up and dance!!!"))  # Вызываем как функцию


# Превратим декоратор printlog из функции в класс
# Так он выглядел сначала
def printlog(func):
    def wrapper(*args, **kwargs):  # может принимать любое число аргументов
        print("CALLING: " + func.__name__)
        return func(*args, **kwargs)

    return wrapper


# И так применялся к функции
@printlog
def buzz(x, y):
    print(x + y)


buzz(10, 20)


# Перепишем декоратор в виде класса
class PrintLog:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        print(f"CALLING: {self.func.__name__}")
        return self.func(*args, **kwargs)


@PrintLog
def buzz(x, y):
    print(x + y)


buzz(50, 60)

Вставить формулу как
Блок
Строка
Дополнительные настройки
Цвет формулы
Цвет текста
#333333
Используйте LaTeX для набора формулы
Предпросмотр
\({}\)
Формула не набрана
Вставить