Программирование и IT

Python: декораторы

Декоратор — это функция, которая принимает другую функцию и возвращает новую, с добавленным поведением. Знак `@` перед определением — просто короткая запись присваивания. Если держать это в голове, вся тема разбирается за один вечер.

Обновлено
В этой статье

Функция — такое же значение, как число#

Без этого дальше не пойдёт: функцию в Python можно положить в переменную,
передать аргументом и вернуть из другой функции.

def shout(text):
    return text.upper()

f = shout                 # без скобок — сама функция, не результат
print(f("привет"))        # ПРИВЕТ
print(list(map(shout, ["a", "b"])))   # ['A', 'B']

Скобки означают вызов. f = shout — это ссылка, f = shout("x") — результат
вызова. Путаница между ними — источник половины ошибок в этой теме.

Простейший декоратор#

Декоратор принимает функцию, определяет внутри обёртку и возвращает её.

def loud(func):
    def wrapper(*args, **kwargs):
        print("вызов", func.__name__)
        result = func(*args, **kwargs)
        print("результат", result)
        return result
    return wrapper

@loud
def add(a, b):
    return a + b

print(add(2, 3))
  # вызов add
  # результат 5
  # 5

Запись @loud над def add равнозначна строке add = loud(add) после
определения. Ничего больше в ней нет.

Обёртка принимает *args, **kwargs, чтобы подойти к функции с любой
сигнатурой, и обязательно возвращает результат — иначе декорированная функция
начнёт молча отдавать None. Это ошибка, которую ищут дольше всего.

Сохранить имя функции#

Без дополнительных мер декорированная функция подменяется обёрткой, и об этом
узнаёт всё, что смотрит на имя и документацию:

print(add.__name__)   # wrapper

Лечится одной строкой — functools.wraps копирует имя, документацию и
аннотации из исходной функции в обёртку:

import functools

def loud(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@loud
def add(a, b):
    """Сложить два числа."""
    return a + b

print(add.__name__, "|", add.__doc__)   # add | Сложить два числа.

Декоратор с аргументами#

Когда у декоратора есть собственные настройки, добавляется третий уровень:
внешняя функция принимает настройки и возвращает сам декоратор.

import functools

def repeat(times):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(times=3)
def hello():
    print("привет")
    return "ок"

print(hello())
  # привет / привет / привет / ок

Отсюда правило: декоратор с аргументами пишется со скобками — @repeat(3), а
без аргументов скобок нет — @loud. Перепутать легко: @repeat без скобок
передаст саму функцию в параметр times, и падение случится не при
объявлении, а при первом вызове.

Порядок и готовые декораторы#

Несколько декораторов применяются снизу вверх — ближний к def оборачивает
первым:

@a
@b
def f():
    ...
  # равнозначно f = a(b(f))

В стандартной библиотеке уже есть полезные декораторы. functools.lru_cache
запоминает результаты вызовов:

import functools

@functools.lru_cache(maxsize=None)
def fib(n):
    return n if n < 2 else fib(n - 1) + fib(n - 2)

print(fib(30))            # 832040
print(fib.cache_info().hits)   # 28

Без кеша тот же fib(30) считался бы больше миллиона раз. Ещё из привычных —
@property, @staticmethod и @classmethod, которые применяются к методам
и разобраны в статье про классы.

Практика на полчаса: напиши декоратор timed, который печатает время
выполнения через time.perf_counter, и декоратор retry(times), повторяющий
вызов при исключении, — второй опирается на
обработку ошибок.

План по этапам

  1. Функция в переменнойПрисвоить функцию переменной без скобок, вызвать через новое имя, сравнить с присваиванием результата.
  2. Обёртка вручнуюНаписать wrapper и применить его строкой f = deco(f), без знака @.
  3. Перевести на @Заменить ручное присваивание на декоратор и убедиться, что поведение то же.
  4. Добавить functools.wrapsПроверить __name__ до и после добавления wraps.
  5. Декоратор с параметромСделать repeat(times) и вызвать функцию с разными значениями параметра.

Начать изучать эту тему у себя

План ляжет в твой репозиторий: отмечай этапы, веди конспект — история изменений покажет, как ты продвинулся.

Начать план

Проверь себя

1.Знак @deco над def f() равнозначен какой строке?

2.Обёртка вызывает func(*args), но не пишет return. Что вернёт декорированная функция?

3.Сколько раз напечатается «привет» при вызове функции, украшенной @repeat(times=3)?

4.В записи «@a / @b / def f()» какой декоратор оборачивает функцию первым — a или b?

Источники

Было полезно?

Ещё темы

Программирование и IT Как изучить Python с нуля Python хорош для первого знакомства с программированием: код читается почти как текст, а стандартная библиотека закрывает большинство бытовых задач. Этот план ведёт от установки интерпретатора до собственных скриптов, покрытых тестами, — примерно за четыре месяца при занятиях около часа в день. Программирование и IT Python: функции Функция — именованный кусок кода, который принимает значения и возвращает результат. В Python объявление занимает одну строку, но у аргументов есть тонкости: умолчания вычисляются один раз, именованные и позиционные смешиваются по правилам, а без return функция всё равно что-то возвращает. Программирование и IT Python: классы Класс описывает, из каких данных состоит объект и что он умеет делать. Конструктор `__init__` заполняет новый объект, `self` — ссылка на него самого. Ниже — минимальный рабочий класс, разница между атрибутом класса и атрибутом экземпляра и способы сделать код короче. Программирование и IT Python: задачи с решениями Подборка задач по возрастанию сложности — от разворота строки до генератора и декоратора-счётчика. Сначала решай сам и только потом открывай разбор: сравнение своего кода с чужим даёт больше, чем чтение готового. Все решения проверены на Python 3. Программирование и IT Python: чтение файла Работа с файлом состоит из трёх шагов: открыть, прочитать или записать, закрыть. Закрытие лучше не делать руками — для этого есть оператор `with`. А главный параметр, про который забывают, — кодировка: без неё один и тот же код на разных машинах читает файл по-разному. Программирование и IT Python: срезы Срез вырезает из последовательности кусок по правилу «с какого индекса, до какого, с каким шагом». Запись компактная, но у неё два места, где почти все ошибаются: правая граница не включается, а при отрицательном шаге границы меняются местами. Разберём оба.

Ещё сценарии