Как не потерять docstring функции, обернутой декоратором?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Сохранение docstring функции при использовании декоратора
Когда функция оборачивается декоратором, она теряет свой оригинальный docstring и другие метаданные (name, module и т.д.). Это происходит потому что декоратор возвращает обертку (wrapper функцию) вместо оригинальной функции. Существует несколько способов решить эту проблему.
1. Проблема: потеря docstring
def my_decorator(func):
def wrapper(*args, **kwargs):
"""Это docstring обертки"""
return func(*args, **kwargs)
return wrapper
@my_decorator
def hello():
"""Это оригинальный docstring функции hello"""
print("Hello World")
print(hello.__doc__) # "Это docstring обертки"
print(hello.__name__) # "wrapper"
# Потеряли оригинальные метаданные!
2. Решение: functools.wraps
Модуль functools предоставляет декоратор @wraps, который копирует метаданные оригинальной функции:
from functools import wraps
def my_decorator(func):
@wraps(func) # Копирует __name__, __doc__, __module__, и др.
def wrapper(*args, **kwargs):
"""Это docstring обертки"""
return func(*args, **kwargs)
return wrapper
@my_decorator
def hello():
"""Это оригинальный docstring функции hello"""
print("Hello World")
print(hello.__doc__) # "Это оригинальный docstring функции hello"
print(hello.__name__) # "hello"
3. Что копирует @wraps
Декоратор @wraps копирует следующие атрибуты:
from functools import wraps
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@decorator
def my_function():
"""Оригинальный docstring"""
pass
# Скопированные атрибуты:
print(my_function.__name__) # "my_function"
print(my_function.__doc__) # "Оригинальный docstring"
print(my_function.__module__) # "__main__"
print(my_function.__qualname__) # "my_function"
print(my_function.__annotations__) # {}
print(my_function.__dict__) # {}
4. Практический пример: логирующий декоратор
from functools import wraps
import logging
def log_calls(func):
"""Декоратор для логирования вызовов функции"""
@wraps(func)
def wrapper(*args, **kwargs):
logging.info(f"Calling {func.__name__} with args={args}, kwargs={kwargs}")
result = func(*args, **kwargs)
logging.info(f"{func.__name__} returned {result}")
return result
return wrapper
@log_calls
def add(a, b):
"""Складывает два числа"""
return a + b
print(add.__doc__) # "Складывает два числа"
print(add.__name__) # "add"
add(5, 3) # Логирует вызов
5. Декоратор с параметрами
Если декоратор принимает параметры, @wraps применяется к самой внутренней функции:
from functools import wraps
def repeat(times):
"""Повторяет выполнение функции N раз"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def greet(name):
"""Приветствует человека"""
print(f"Hello, {name}!")
print(greet.__doc__) # "Приветствует человека"
print(greet.__name__) # "greet"
greet("Alice") # Выполнится 3 раза
6. Декоратор класса
Для декораторов класса также используется @wraps:
from functools import wraps
def validate_input(cls):
"""Добавляет валидацию входных данных"""
original_init = cls.__init__
@wraps(original_init)
def new_init(self, *args, **kwargs):
# Валидация
print("Validating input...")
original_init(self, *args, **kwargs)
cls.__init__ = new_init
return cls
@validate_input
class User:
"""Класс для представления пользователя"""
def __init__(self, name, age):
self.name = name
self.age = age
print(User.__doc__) # "Класс для представления пользователя"
7. Цепочка декораторов с @wraps
from functools import wraps
def timing(func):
@wraps(func)
def wrapper(*args, **kwargs):
import time
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"Took {end - start:.4f} seconds")
return result
return wrapper
def validate(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("Validating...")
return func(*args, **kwargs)
return wrapper
@timing
@validate
def process_data(data):
"""Обрабатывает данные"""
return sum(data)
print(process_data.__name__) # "process_data"
print(process_data.__doc__) # "Обрабатывает данные"
process_data([1, 2, 3, 4, 5])
8. Ручное копирование атрибутов (без @wraps)
Если по какой-то причине нельзя использовать @wraps:
def my_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
# Ручное копирование атрибутов
wrapper.__name__ = func.__name__
wrapper.__doc__ = func.__doc__
wrapper.__module__ = func.__module__
wrapper.__qualname__ = func.__qualname__
wrapper.__annotations__ = func.__annotations__
wrapper.__dict__.update(func.__dict__)
return wrapper
@my_decorator
def hello():
"""Оригинальный docstring"""
pass
print(hello.__doc__) # "Оригинальный docstring"
print(hello.__name__) # "hello"
9. Сохранение signature функции
Для сохранения сигнатуры функции (параметры и типы) используй inspect:
from functools import wraps
import inspect
def my_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
# Сохранить signature
wrapper.__signature__ = inspect.signature(func)
return wrapper
@my_decorator
def add(a: int, b: int) -> int:
"""Складывает два числа"""
return a + b
sig = inspect.signature(add)
print(sig) # (a: int, b: int) -> int
10. Декоратор с сохранением типов
from functools import wraps
from typing import TypeVar, Callable
F = TypeVar('F', bound=Callable)
def my_decorator(func: F) -> F:
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@my_decorator
def calculate(x: int, y: int) -> int:
"""Вычисляет сумму"""
return x + y
# IDE и type checkers будут понимать типы функции
11. Практический пример: auth декоратор
from functools import wraps
def require_auth(func):
"""Требует аутентификацию перед вызовом функции"""
@wraps(func)
def wrapper(*args, **kwargs):
# Проверка аутентификации
user = get_current_user() # гипотетическая функция
if not user:
raise PermissionError("User not authenticated")
return func(*args, **kwargs)
return wrapper
@require_auth
def delete_account(account_id):
"""Удаляет аккаунт пользователя"""
# логика удаления
pass
print(delete_account.__doc__) # "Удаляет аккаунт пользователя"
print(delete_account.__name__) # "delete_account"
Ключевые правила
✅ ВСЕГДА используй @wraps — это лучшая практика ✅ Импортируй из functools — встроенный модуль ✅ Применяй к самой внутренней функции — той что возвращается ✅ Копирует метаданные — name, doc, module, dict ✅ Работает с любыми функциями — обычные, с параметрами, методы классов ✅ Не требует ручного копирования — @wraps делает все за тебя
Использование @wraps — стандарт при написании декораторов в Python.