← Назад к вопросам

Почему нельзя сделать декоратор с помощью одной функции в Python?

1.8 Middle🔥 161 комментариев
#Python Core

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Почему нельзя сделать декоратор с помощью одной функции в Python?

На самом деле, это неправда! Можно сделать декоратор с помощью одной функции, если её организовать как вложенные функции. Правильнее будет сказать: "Почему декоратор требует вложенных функций?". Давайте разберёмся, почему это необходимо.

Основная проблема

Декоратор должен:

  1. Получить функцию-оригинал (которую декорируем)
  2. Создать новую функцию (обёртку)
  3. Вернуть новую функцию вместо оригинала
  4. Сохранить способность вызывать оригинальную функцию

Это требует нескольких уровней функций.

Попытка с одной функцией (НЕПРАВИЛЬНО)

# НЕРАБОТАЮЩИЙ подход
def simple_decorator(func):
    print(f"Декорирую {func.__name__}")
    return func  # Просто возвращаем оригинальную функцию

@simple_decorator
def greet(name):
    return f"Hello, {name}!"

print(greet("Alice"))  # Hello, Alice!
# Это работает, но декоратор ничего НЕ делает!
# Он не обёртывает функцию

Проблема: Мы не можем добавить поведение вокруг функции без вложенной функции.

Правильный подход: вложенная функция

# ПРАВИЛЬНЫЙ подход с одной функцией (и вложенной функцией)
def logging_decorator(func):
    # func — это функция, которую мы декорируем
    # Но где её вызывать? Нужна обёртка!
    
    def wrapper(*args, **kwargs):  # Вложенная функция
        print(f"Вызов: {func.__name__}")
        result = func(*args, **kwargs)  # Вызываем оригинальную функцию
        print(f"Результат: {result}")
        return result
    
    return wrapper  # Возвращаем обёртку, а не оригинал

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

add(2, 3)  # Это работает!
# Вывод:
# Вызов: add
# Результат: 5

Почему нужны вложенные функции?

# Визуальное объяснение

# Уровень 1: Декоратор получает функцию
def logging_decorator(func):
    print(f"[Уровень 1] Получена функция: {func.__name__}")
    
    # Уровень 2: Обёртка, которая вызывается при каждом вызове
    def wrapper(*args, **kwargs):
        print(f"[Уровень 2] Запускаем {func.__name__}")
        result = func(*args, **kwargs)
        print(f"[Уровень 2] {func.__name__} завершилась")
        return result
    
    return wrapper  # Возвращаем обёртку

# Применение
@logging_decorator  # Вызывается ОДИН раз при определении функции
def greet(name):
    return f"Hello, {name}!"

# greet теперь указывает на wrapper!
greet("Alice")  # wrapper вызывается КАЖДЫЙ раз

Аналогия: Упаковка посылки

┌─────────────────────────────────────────────┐
│        Декоратор (logging_decorator)        │
├─────────────────────────────────────────────┤
│                                             │
│  Вход: def greet(name):                    │
│                                             │
│  1. Декоратор получает оригинальную функцию│
│  2. Создаёт НОВУЮ функцию (wrapper)        │
│  3. wrapper содержит оригинальную функцию  │
│  4. wrapper добавляет логику вокруг        │
│  5. Возвращает wrapper вместо оригинала    │
│                                             │
│  Выход: def wrapper(*args, **kwargs):      │
│                                             │
└─────────────────────────────────────────────┘

Практические примеры

1. Таймер (timing decorator)

import time

def timing_decorator(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        elapsed = time.time() - start
        print(f"⏱️ {func.__name__} выполнена за {elapsed:.4f} сек")
        return result
    return wrapper

@timing_decorator
def slow_function():
    time.sleep(1)
    return "Готово"

slow_function()  # ⏱️ slow_function выполнена за 1.0001 сек

2. Кеширование (caching decorator)

def memoize_decorator(func):
    cache = {}  # Словарь для кеширования результатов
    
    def wrapper(*args, **kwargs):
        # Создаём ключ кеша
        key = (args, tuple(sorted(kwargs.items())))
        
        if key in cache:
            print(f"📦 Из кеша: {func.__name__}")
            return cache[key]
        
        print(f"🔄 Вычисляю: {func.__name__}")
        result = func(*args, **kwargs)
        cache[key] = result
        return result
    
    return wrapper

@memoize_decorator
def fibonacci(n):
    if n <= 1:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

print(fibonacci(5))  # 🔄 Много вычислений
print(fibonacci(5))  # 📦 Из кеша

3. Проверка прав доступа

def requires_admin(func):
    def wrapper(user, *args, **kwargs):
        if user.role != 'admin':
            raise PermissionError(f"User {user.name} is not admin")
        
        print(f"✅ {user.name} имеет доступ")
        return func(user, *args, **kwargs)
    
    return wrapper

class User:
    def __init__(self, name, role):
        self.name = name
        self.role = role

@requires_admin
def delete_user(user, target_id):
    print(f"Удаляю пользователя {target_id}")
    return f"User {target_id} deleted"

admin = User("Alice", "admin")
user = User("Bob", "user")

delete_user(admin, 123)  # ✅ Alice имеет доступ, Удаляю пользователя 123
try:
    delete_user(user, 123)  # ❌ PermissionError
except PermissionError as e:
    print(f"Ошибка: {e}")

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

import time
import random

def retry(max_attempts=3, delay=1):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for attempt in range(1, max_attempts + 1):
                try:
                    print(f"Попытка {attempt}/{max_attempts}...")
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == max_attempts:
                        print(f"❌ Не удалось после {max_attempts} попыток")
                        raise
                    print(f"⚠️ Ошибка: {e}, жду {delay}сек...")
                    time.sleep(delay)
        return wrapper
    return decorator

@retry(max_attempts=3, delay=1)
def unstable_api():
    if random.random() < 0.8:
        raise ConnectionError("API не доступен")
    return "✅ Успешно"

try:
    result = unstable_api()
    print(result)
except ConnectionError:
    print("Окончательная ошибка")

5. Валидация аргументов

def validate_args(func):
    def wrapper(*args, **kwargs):
        # Проверяем аргументы
        if not isinstance(args[0], (int, float)):
            raise TypeError(f"Первый аргумент должен быть числом, получен {type(args[0])}")
        
        return func(*args, **kwargs)
    
    return wrapper

@validate_args
def square(x):
    return x ** 2

print(square(5))  # 25
try:
    print(square("text"))  # TypeError
except TypeError as e:
    print(f"Ошибка: {e}")

Декоратор с параметрами (требует ещё один уровень вложенности)

# Три уровня: параметры → декоратор → обёртка

def repeat(times):
    def decorator(func):
        def wrapper(*args, **kwargs):
            results = []
            for i in range(times):
                print(f"Запуск {i+1}/{times}")
                results.append(func(*args, **kwargs))
            return results
        return wrapper
    return decorator

@repeat(times=3)  # repeat(times=3) возвращает decorator
def greet(name):  # decorator(greet) возвращает wrapper
    return f"Hello, {name}!"

print(greet("Alice"))
# Запуск 1/3
# Запуск 2/3
# Запуск 3/3
# ['Hello, Alice!', 'Hello, Alice!', 'Hello, Alice!']

Почему именно вложенные функции?

1. Closures (замыкания)

def outer(x):
    def inner(y):
        return x + y  # inner помнит x из outer
    return inner

add_5 = outer(5)
print(add_5(3))  # 8 (помнит x=5)
print(add_5(10)) # 15 (всё ещё помнит x=5)

2. Каждый декоратор создаёт свою область видимости

def logging_decorator(func):
    # func доступна в closure
    def wrapper(*args, **kwargs):
        # func всё ещё доступна здесь
        return func(*args, **kwargs)
    return wrapper

Попытка сделать без вложенных функций (НЕ РАБОТАЕТ)

# НЕПРАВИЛЬНО
original_function = None

def bad_decorator(func):
    global original_function
    original_function = func  # Где хранить?
    return original_function

# Проблемы:
# 1. Глобальные переменные — плохая практика
# 2. Нет способа добавить логику вокруг функции
# 3. Конфликты при нескольких декораторах

Лучшие практики

Используйте functools.wraps для сохранения метаданных:

import functools

def my_decorator(func):
    @functools.wraps(func)  # Копирует __name__, __doc__ и т.д.
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

Используйте типизацию:

from typing import Callable, Any

def typed_decorator(func: Callable[..., Any]) -> Callable[..., Any]:
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

Документируйте декораторы:

def useful_decorator(func):
    """Декоратор, который делает полезные вещи.
    
    Args:
        func: Функция для декорирования
    
    Returns:
        Обёрнутая функция
    """
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

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