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

Почему аргумент decorator не подчищается?

1.0 Junior🔥 111 комментариев
#Асинхронность и многопоточность

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

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

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

Проблема с сборкой мусора и аргументами decorator'а

Аргумент decorator не подчищается сборщиком мусора из-за циклических ссылок между функцией-обёрткой и исходной функцией. Python'овский GC не может автоматически разрешить такие циклы в некоторых случаях.

Как это происходит

def my_decorator(func):
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

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

Здесь создаётся циклическая ссылка:

  • wrapper удерживает ссылку на func через замыкание
  • Объект функции greet удерживает ссылку на wrapper как свой __wrapped__ атрибут
  • Если где-то есть ещё одна ссылка на greet, возникает цикл

Почему GC их не удаляет

Сборщик мусора Python (CPython) использует reference counting как первый уровень очистки. Когда счётчик ссылок падает до нуля, объект удаляется немедленно. Но если есть циклические ссылки, счётчик никогда не будет нулевым.

def create_decorator():
    def decorator(func):
        def wrapper(*args, **kwargs):
            return func(*args, **kwargs)
        wrapper.__wrapped__ = func  # Циклическая ссылка
        return wrapper
    return decorator

decorator_inst = create_decorator()

@decorator_inst
def func1():
    pass

# func1 → wrapper → func
# func → wrapper.__wrapped__ → func (цикл!)

Решения

1. Используй functools.wraps

import functools

def my_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

functools.wraps правильно обрабатывает метаданные и не создаёт лишних циклических ссылок.

2. Явная очистка при удалении

class Decorator:
    def __init__(self, func):
        self.func = func
        self.wrapper = self._create_wrapper()
    
    def _create_wrapper(self):
        def wrapper(*args, **kwargs):
            return self.func(*args, **kwargs)
        return wrapper
    
    def __del__(self):
        # Явно разрушаем ссылки
        self.func = None
        self.wrapper = None

3. Используй weakref для слабых ссылок

import weakref
import functools

def my_decorator(func):
    # Слабая ссылка на функцию
    func_ref = weakref.ref(func)
    
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        f = func_ref()
        if f is None:
            raise RuntimeError("Original function was garbage collected")
        return f(*args, **kwargs)
    
    return wrapper

4. Отключи циклический мусор вручную

import gc

# Для критичных операций, где нужна предсказуемость
gc.disable()

try:
    # Твой код
    pass
finally:
    gc.enable()

Практический пример: Когда это критично

Это особенно важно при:

  • Работе с большим количеством временных функций (генерирование динамических функций)
  • Долгоживущих приложениях (серверы, демоны), где утечки памяти накапливаются
  • Встраивании Python в другие системы
# Проблема: утечка памяти при создании множества декораторов
for i in range(1000000):
    @my_decorator
    def temp_func():
        return i
    # temp_func удаляется, но память не освобождается сразу

Вывод

Используй functools.wraps — это стандарт и решает большинство проблем. Если нужна более агрессивная очистка, рассмотри слабые ссылки или явную очистку в __del__. Помни, что в CPython reference counting обычно справляется с циклами при удалении переменных, но в других реализациях (PyPy, Jython) поведение может отличаться.

Почему аргумент decorator не подчищается? | PrepBro