Зачем нужен декоратор, если можно написать функцию в не класса?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Декораторы 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
Итоги
Декораторы нужны, потому что они:
- Переиспользуемы — применяются к множеству функций
- Сохраняют метаинформацию —
__name__,__doc__, аннотации типов - Комбинируются — можно применять несколько декораторов
- Работают с фреймворками — FastAPI, Flask, SQLAlchemy полагаются на метаинформацию
- Читаемы —
@decoratorпонятнее, чем вложенные функции - DRY — избегаем дублирования кода
Без functools.wraps декоратор теряет свой смысл — это же просто обёртка без информации. С functools.wraps декоратор становится мощным инструментом для разделения ответственности и переиспользования кода.