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

Как не потерять docstring функции, обернутой декоратором?

1.6 Junior🔥 131 комментариев
#Python Core

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

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

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

Сохранение 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.