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

Зачем нужен декоратор, если можно написать функцию в не класса?

1.2 Junior🔥 111 комментариев
#Python Core#Soft Skills

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

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

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

Декораторы vs функции: когда использовать что

Этот вопрос показывает глубокое понимание Python. Действительно, многое можно сделать и так, и так. Но есть ключевые различия, которые делают декораторы не просто красивым синтаксисом.

Проблема: функция внутри функции vs декоратор

Сначала посмотрим на кажущуюся эквивалентность:

# ВАРИАНТ 1: Функция внутри функции
def log_and_execute_v1(func):
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        result = func(*args, **kwargs)
        print(f"Done {func.__name__}")
        return result
    return wrapper

def my_func_v1(x):
    return x * 2

my_func_v1 = log_and_execute_v1(my_func_v1)
result = my_func_v1(5)

# ВАРИАНТ 2: Декоратор (синтаксический сахар!)
def log_and_execute_v2(func):
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        result = func(*args, **kwargs)
        print(f"Done {func.__name__}")
        return result
    return wrapper

@log_and_execute_v2  # ← Это просто синтаксический сахар
def my_func_v2(x):
    return x * 2

result = my_func_v2(5)

Фактически, @log_and_execute_v2 — это просто синтаксический сахар для my_func_v2 = log_and_execute_v2(my_func_v2).

Ключевые различия

1. Применимость к разным типам

Декораторы могут применяться ко многим функциям без повторения кода.

# Один декоратор
def retry(max_attempts=3):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == max_attempts - 1:
                        raise
        return wrapper
    return decorator

# Применяем к разным функциям
@retry(max_attempts=3)
async def fetch_from_api():
    return await http.get("https://api.example.com")

@retry(max_attempts=5)
async def query_database():
    return await db.query("SELECT * FROM users")

# ВАРИАНТ 2: функция внутри класса (спагетти код)
class APIHandler:
    async def fetch_from_api(self):
        for attempt in range(3):
            try:
                return await http.get("https://api.example.com")
            except Exception as e:
                if attempt == 2:
                    raise

class DatabaseHandler:
    async def query_database(self):
        for attempt in range(5):
            try:
                return await db.query("SELECT * FROM users")
            except Exception as e:
                if attempt == 4:
                    raise

# Дублирование логики retry!

2. Метаинформация (Introspection)

Декораторы с functools.wraps сохраняют оригинальную информацию о функции.

import functools

# ПРАВИЛЬНО: с functools.wraps
def log_decorator(func):
    @functools.wraps(func)  # ← ВАЖНО!
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

@log_decorator
def process_data(x: int) -> int:
    """Обрабатывает данные x и возвращает результат"""
    return x * 2

print(process_data.__name__)  # "process_data" ✓
print(process_data.__doc__)   # "Обрабатывает данные..." ✓

# БЕЗ functools.wraps
def bad_decorator(func):
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

@bad_decorator
def process_data_bad(x: int) -> int:
    """Обрабатывает данные"""
    return x * 2

print(process_data_bad.__name__)  # "wrapper" ✗ (потеряна информация!)
print(process_data_bad.__doc__)   # None ✗

Это критично для:

  • IDE автодополнения — IDE видит реальное имя функции
  • Документации — docstring сохраняется
  • Отладки — stack trace показывает реальное имя
  • Introspection библиотек — FastAPI, SQLAlchemy полагаются на это

3. Стек вызовов при отладке

import functools

def decorator_with_wraps(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print("Before")
        return func(*args, **kwargs)
    return wrapper

def decorator_without_wraps(func):
    def wrapper(*args, **kwargs):
        print("Before")
        return func(*args, **kwargs)
    return wrapper

@decorator_with_wraps
def my_function():
    raise ValueError("Error!")

# Stack trace:
# Traceback (most recent call last):
#   File "test.py", line 10, in <module>
#     my_function()  ← Видим реальное имя
#   File "test.py", line 5, in my_function
#     raise ValueError("Error!")

@decorator_without_wraps
def another_function():
    raise ValueError("Error!")

# Stack trace:
# Traceback (most recent call last):
#   File "test.py", line 10, in <module>
#     another_function()
#   File "test.py", line 15, in wrapper  ← Неясно, какая функция
#     raise ValueError("Error!")

4. Работа с FastAPI и другими фреймворками

from fastapi import FastAPI, Depends

app = FastAPI()

# ДЕКОРАТОР ПОДХОДА
def require_admin(func):
    @functools.wraps(func)
    async def wrapper(current_user: User = Depends(get_current_user)):
        if not current_user.is_admin:
            raise PermissionError()
        return await func(current_user)
    return wrapper

@app.get("/admin")
@require_admin  # ← Работает с FastAPI благодаря functools.wraps
async def admin_panel(current_user: User):
    return {"admin": current_user.name}

# FastAPI может introspect функцию и увидеть правильные параметры!

5. Множественные декораторы

Декораторы можно комбинировать, что невозможно с функциями-обёртками.

def authenticate(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        token = request.headers.get('Authorization')
        if not verify_token(token):
            raise Unauthorized()
        return func(*args, **kwargs)
    return wrapper

def log_action(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        logger.info(f"Executing {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

def cache_result(ttl=300):
    def decorator(func):
        cache = {}
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            key = (args, tuple(kwargs.items()))
            if key in cache:
                return cache[key]
            result = func(*args, **kwargs)
            cache[key] = result
            return result
        return wrapper
    return decorator

# Комбинируем несколько декораторов
@cache_result(ttl=600)
@log_action
@authenticate
async def get_user_profile(user_id: int):
    return await db.get_user(user_id)

# Это эквивалентно:
# get_user_profile = cache_result(600)(log_action(authenticate(get_user_profile)))

# С функциями-обёртками пришлось бы:
get_user_profile = authenticate(get_user_profile)
get_user_profile = log_action(get_user_profile)
get_user_profile = cache_result(600)(get_user_profile)
# Или вложенные функции — кошмар для читаемости

Настоящие кейсы использования

Кейс 1: Валидация параметров

# С декоратором — чистый и переиспользуемый
def validate_email(func):
    @functools.wraps(func)
    def wrapper(email: str, *args, **kwargs):
        if '@' not in email:
            raise ValueError(f"Invalid email: {email}")
        return func(email, *args, **kwargs)
    return wrapper

@validate_email
def send_notification(email: str, message: str):
    pass

@validate_email
def subscribe_newsletter(email: str):
    pass

# С функцией-обёрткой в классе — дублирование
class NotificationService:
    def send_notification(self, email: str):
        if '@' not in email:
            raise ValueError()
        # логика
    
    def subscribe_newsletter(self, email: str):
        if '@' not in email:
            raise ValueError()
        # логика

Кейс 2: Управление ресурсами

# С декоратором
def with_db_session(func):
    @functools.wraps(func)
    async def wrapper(*args, **kwargs):
        async with get_session() as session:
            return await func(session, *args, **kwargs)
    return wrapper

@with_db_session
async def get_users(session: AsyncSession):
    return await session.query(User).all()

@with_db_session
async def delete_old_posts(session: AsyncSession):
    await session.execute(delete(Post).where(Post.created_at < old_date))

# С функцией в классе:
class UserRepository:
    async def get_users(self):
        async with get_session() as session:
            return await session.query(User).all()
    
    async def delete_old_posts(self):
        async with get_session() as session:
            await session.execute(...)
    
    # Дублирование context manager

Когда НЕ нужны декораторы

# ❌ Слишком усложняет
def unnecessary_decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

# ✅ Просто используй функцию
def simple_function():
    pass

# ❌ Логирование, которое вызывается один раз
@log_decorator
def initialize_app():
    pass

# ✅ Просто логируй явно
def initialize_app():
    logger.info("Starting app")
    pass

Итоги

Декораторы нужны, потому что они:

  1. Переиспользуемы — применяются к множеству функций
  2. Сохраняют метаинформацию__name__, __doc__, аннотации типов
  3. Комбинируются — можно применять несколько декораторов
  4. Работают с фреймворками — FastAPI, Flask, SQLAlchemy полагаются на метаинформацию
  5. Читаемы@decorator понятнее, чем вложенные функции
  6. DRY — избегаем дублирования кода

Без functools.wraps декоратор теряет свой смысл — это же просто обёртка без информации. С functools.wraps декоратор становится мощным инструментом для разделения ответственности и переиспользования кода.