ШПАРГАЛКА

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

Декораторы в Python: замыкания, синтаксис @, functools.wraps, декораторы с аргументами и на классах, логирование, замер времени, кэширование.

Декоратор — это функция, которая принимает другую функцию (или класс) и возвращает новую, обычно расширяя её поведение, не меняя исходный код. Чтобы понять декораторы, нужно сначала разобраться с двумя вещами: функции как объекты первого класса и замыкания. Дальше идём от простого синтаксиса @ к декораторам с аргументами и практическим примерам.

Функции — объекты первого класса

В Python функция — это обычный объект. Её можно присвоить переменной, передать в другую функцию как аргумент и вернуть из функции. Именно это делает декораторы возможными.

def greet(name):
    return f"Привет, {name}!"

# присваиваем функцию переменной (без скобок — не вызываем)
say = greet
print(say("Аня"))   # Привет, Аня!

# функция как аргумент другой функции
def call_twice(func, value):
    return func(value) + " " + func(value)

print(call_twice(greet, "Боб"))  # Привет, Боб! Привет, Боб!

У функции есть атрибуты — например __name__ и __doc__. Они пригодятся позже, когда речь зайдёт о functools.wraps.

print(greet.__name__)  # greet

Замыкания (closures)

Вложенная функция может «запоминать» переменные из внешней функции даже после того, как внешняя функция завершила работу. Такая связка вложенной функции с захваченным окружением и называется замыканием.

def make_multiplier(factor):
    def multiply(x):
        return x * factor   # factor захвачен из внешней области
    return multiply

double = make_multiplier(2)
triple = make_multiplier(3)

print(double(10))  # 20
print(triple(10))  # 30

Здесь double помнит factor = 2, а triplefactor = 3. Декоратор — это, по сути, частный случай замыкания: внешняя функция захватывает декорируемую функцию.

Что такое декоратор

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

def shout(func):
    def wrapper(name):
        result = func(name)
        return result.upper()
    return wrapper

def greet(name):
    return f"привет, {name}"

greet = shout(greet)   # оборачиваем вручную
print(greet("аня"))    # ПРИВЕТ, АНЯ

Мы заменили greet на wrapper, который вызывает оригинал и дорабатывает результат.

Синтаксис @

Запись @decorator над определением функции — это просто короткая форма для func = decorator(func). Два примера ниже полностью эквивалентны.

# с синтаксисом @
@shout
def greet(name):
    return f"привет, {name}"

# то же самое без @
def greet(name):
    return f"привет, {name}"
greet = shout(greet)

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

Простой декоратор-обёртка

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

def log_call(func):
    def wrapper(*args, **kwargs):
        print(f"Вызов {func.__name__} с args={args}, kwargs={kwargs}")
        result = func(*args, **kwargs)
        print(f"Результат: {result}")
        return result
    return wrapper

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

add(2, 3)
# Вызов add с args=(2, 3), kwargs={}
# Результат: 5

Важно: wrapper обязательно возвращает результат оригинальной функции, иначе декорированная функция начнёт возвращать None.

functools.wraps — зачем он нужен

После декорирования имя и докстринг функции «теряются»: вместо них видны данные внутренней wrapper. Это ломает интроспекцию, документацию и отладку.

def log_call(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@log_call
def add(a, b):
    """Складывает два числа."""
    return a + b

print(add.__name__)  # wrapper   (а ожидали add!)
print(add.__doc__)   # None

Решение — обернуть wrapper декоратором functools.wraps(func). Он копирует __name__, __doc__ и другие метаданные с оригинала.

import functools

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

@log_call
def add(a, b):
    """Складывает два числа."""
    return a + b

print(add.__name__)  # add
print(add.__doc__)   # Складывает два числа.

Правило: всегда добавляйте @functools.wraps(func) к своим декораторам.

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

Иногда декоратор нужно настраивать. Тогда добавляют ещё один уровень вложенности: внешняя функция принимает аргументы и возвращает сам декоратор. Получается три уровня функций.

import functools

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

@repeat(times=3)
def hello():
    print("Привет!")

hello()
# Привет!
# Привет!
# Привет!

Запись @repeat(times=3) сначала вызывает repeat(3) — это возвращает decorator, который затем применяется к hello.

Несколько декораторов и порядок применения

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

import functools

def bold(func):
    @functools.wraps(func)
    def wrapper(*a, **k):
        return "" + func(*a, **k) + ""
    return wrapper

def italic(func):
    @functools.wraps(func)
    def wrapper(*a, **k):
        return "" + func(*a, **k) + ""
    return wrapper

@bold
@italic
def text():
    return "привет"

print(text())  # привет

Эта запись эквивалентна text = bold(italic(text)). Сначала навешивается italic (он ближе к функции), потом bold оборачивает уже декорированную версию.

ЗаписьЧто эквивалентноРезультат
@bold + @italicbold(italic(text))<b><i>...</i></b>
@italic + @bolditalic(bold(text))<i><b>...</b></i>

Декораторы на классах

Здесь есть два разных случая, которые легко перепутать.

Декорирование методов класса

Обычный декоратор-функция отлично работает и с методами — просто помните про self в *args.

import functools

def log_call(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"-> {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

class Account:
    def __init__(self):
        self.balance = 0

    @log_call
    def deposit(self, amount):
        self.balance += amount
        return self.balance

Account().deposit(100)  # -> deposit

Декоратор в виде класса

Декоратором может быть и сам класс — если у него есть метод __call__. Это удобно, когда декоратору нужно хранить состояние.

import functools

class CountCalls:
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
        self.count = 0

    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"Вызов №{self.count}")
        return self.func(*args, **kwargs)

@CountCalls
def say_hi():
    print("Привет")

say_hi()  # Вызов №1 / Привет
say_hi()  # Вызов №2 / Привет
print(say_hi.count)  # 2

Практика: логирование

Базовый и самый частый сценарий — логировать вызовы и результаты функций, не засоряя их тело.

import functools

def logged(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"[LOG] {func.__name__}({args}, {kwargs})")
        result = func(*args, **kwargs)
        print(f"[LOG] -> {result}")
        return result
    return wrapper

@logged
def multiply(a, b):
    return a * b

multiply(4, 5)
# [LOG] multiply((4, 5), {})
# [LOG] -> 20

Практика: замер времени

Декоратор-таймер измеряет, сколько выполняется функция. Полезно для профилирования.

import functools
import time

def timeit(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__} заняла {elapsed:.4f} с")
        return result
    return wrapper

@timeit
def slow_sum(n):
    return sum(range(n))

slow_sum(1_000_000)
# slow_sum заняла 0.0123 с

Практика: кэширование через @lru_cache

В стандартной библиотеке уже есть готовый декоратор кэширования — functools.lru_cache. Он запоминает результаты по аргументам и при повторном вызове не пересчитывает. Особенно заметно на рекурсии.

import functools

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

print(fib(35))            # 9227465  — мгновенно
print(fib.cache_info())   # CacheInfo(hits=33, misses=36, ...)

В Python 3.9+ есть упрощённый @functools.cache — это то же, что lru_cache(maxsize=None). Кэшируемые аргументы должны быть хешируемыми (например, числа и строки, но не списки).

Практика: проверка прав и повтор при ошибке

Декораторы удобны для «сквозной» логики — авторизации и устойчивости к сбоям.

Проверка прав

import functools

def require_admin(func):
    @functools.wraps(func)
    def wrapper(user, *args, **kwargs):
        if not user.get("is_admin"):
            raise PermissionError("Нужны права администратора")
        return func(user, *args, **kwargs)
    return wrapper

@require_admin
def delete_post(user, post_id):
    return f"Пост {post_id} удалён"

print(delete_post({"is_admin": True}, 42))   # Пост 42 удалён
# delete_post({"is_admin": False}, 42)  ->  PermissionError

Повтор при ошибке (retry)

import functools
import time

def retry(times=3, delay=0.5):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            last_error = None
            for attempt in range(1, times + 1):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    last_error = e
                    print(f"Попытка {attempt} не удалась: {e}")
                    time.sleep(delay)
            raise last_error
        return wrapper
    return decorator

@retry(times=3, delay=1)
def fetch_data():
    raise ConnectionError("сеть недоступна")

# fetch_data()
# Попытка 1 не удалась: сеть недоступна
# Попытка 2 не удалась: сеть недоступна
# Попытка 3 не удалась: сеть недоступна
# -> ConnectionError

Шпаргалка по шаблонам

ЗадачаШаблон
Простой декораторdef deco(func): ... return wrapper
Сохранить метаданные@functools.wraps(func) над wrapper
Декоратор с аргументамитри уровня: deco(args) -> decorator -> wrapper
Декоратор с состояниемкласс с __call__
Готовый кэш@functools.lru_cache / @functools.cache

Главное, что стоит запомнить: декоратор — это func = decorator(func), обёртка должна возвращать результат, а functools.wraps добавляется почти всегда.

Поддержать проект