Кэш Python удерживает объекты: разбираем lru_cache на методе

Класс клиента создаётся для каждого задания. В нём есть маленький метод с lru_cache, чтобы повторно не разбирать одну и ту же настройку. Задание давно закончено, переменная с клиентом удалена, а экземпляры продолжают жить. Кэш работает именно так, как его попросили: сохраняет аргументы вызова. Для метода одним из этих аргументов оказывается self.

Что именно становится ключом

Вызов client.parse("compact") выглядит как один строковый аргумент. На уровне функции ему соответствует parse(client, "compact"). Декоратор хранит комбинацию аргументов вместе с результатом. Пока запись есть в кэше, сохраняется ссылка на client. Если у экземпляра есть большие буферы, соединения или граф связанных объектов, маленькая запись может удерживать значительно больше памяти, чем занимает вычисленный ответ.

Особенно заметен эффект при maxsize=None: старые записи не вытесняются по количеству. Но конечный maxsize также требует понимания. Он ограничивает число записей общего кэша функции, а не выделяет отдельный бюджет каждому экземпляру. Десять временных клиентов с разными self занимают разные ключи, даже если передают одну и ту же строку. Сначала нужно определить, от чего результат действительно зависит.

Проверка времени жизни через слабую ссылку

import gc
import weakref
from functools import lru_cache

class Client:
    @lru_cache(maxsize=8)
    def parse(self, text):
        return tuple(part.strip() for part in text.split(","))

client = Client()
assert client.parse("a, b") == ("a", "b")
reference = weakref.ref(client)
del client
gc.collect()
assert reference() is not None
print("before clear:", reference() is not None)
Client.parse.cache_clear()
gc.collect()
assert reference() is None
print("after clear:", reference() is not None)

Слабая ссылка позволяет проверить существование объекта, не удерживая его сама. Сначала она всё ещё находит экземпляр, после очистки — уже нет. Из этого следует узкий и полезный вывод: в данном примере причиной жизни клиента была запись в кэше. Мы не измеряли память большого приложения, зато получили воспроизводимое объяснение конкретной ссылки.

Выберите, кому принадлежит кэш

В примере разбор строки не использует состояние Client. Его можно вынести в отдельную чистую функцию и кэшировать только text. Тогда все клиенты используют общий результат и не становятся частью ключа. Это хорошо, если результат неизменяемый и одна строка всегда означает одно значение. Возвращаемый tuple помогает не допустить случайного изменения разделяемого ответа.

Если результат зависит от настройки клиента, включите в ключ саму настройку в неизменяемом виде. Например, идентификатор формата и его версию. Передача целого объекта лишь ради доступа к одному полю скрывает настоящую зависимость. Если настройка меняется, старое значение ключа не должно выдавать ответ по новым правилам. Кэширование требует ответа и на вопрос актуальности, даже когда проблема впервые проявилась как расход памяти.

Когда кэш действительно должен жить ровно столько же, сколько клиент, храните его в самом экземпляре и определите предел. Для одного вычисляемого свойства можно рассмотреть cached_property, если структура класса это позволяет. Для нескольких аргументов потребуется отдельное решение. Важно, чтобы внешний долгоживущий объект не продолжал удерживать владельца такого кэша. Выбор зависит от области совместного использования и от времени жизни данных.

Почему cache_clear не стоит ставить после каждого запроса

Очистка в примере доказывает механизм. В приложении Client.parse.cache_clear() очистит общий кэш декорированного метода. Если другие запросы используют его одновременно, вы сбросите и их полезные записи. Постоянная очистка может убрать симптом и одновременно отменить пользу кэширования. Сначала исправьте владельца или ключ, затем проверяйте число попаданий и промахов на характерной работе.

Аргументы и результаты lru_cache остаются доступными до вытеснения или очистки. Для собственного сервиса добавьте проверку освобождения временного клиента, как в примере, и проверку результата после смены настроек. Эти два сценария защищают разные свойства: время жизни объекта и правильность ответа. Одна метрика hit rate не подтверждает оба.

Ещё один вопрос — допустимость хранения результата между пользователями. Если метод зависит от прав доступа, локали или настроек организации, один только текст не задаёт полный ключ. Перенос в общую функцию должен сопровождаться явным перечислением таких зависимостей. Иначе исправление памяти создаст ошибку правильности. Проверьте две конфигурации с одинаковым текстовым аргументом и разными ожидаемыми ответами. Если функция действительно чистая и контекст ей не нужен, одинаковый результат для двух клиентов становится полезной проверкой выбранной области совместного использования.

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

От декоратора к устройству приложения

Платный курс «Python для профессионалов» соединяет декораторы методов и классов, functools, подсчёт ссылок, сборку мусора и профилирование памяти. Эта связка полезна, когда знакомый приём языка неожиданно меняет время жизни объектов. Вы сможете перейти от использования готового декоратора к пониманию того, какие ссылки и вызовы он добавляет. Откройте программу курса и продолжите с разделов о декораторах и стандартной библиотеке.

Продолжить на практике

  • Python для профессионалов ↗

    Python для профессионалов — курс для людей, стремящихся углубить свои знания языка. Вы изучите итераторы, генераторы, декораторы, современные возможности Python (3.8+), освоите метапрограммирование, асинхронность, внутреннее устройство интерпретатора, инструменты оптимизации и профилирования, а также получите уверенность в использовании продвинутых возможностей стандартной библиотеки.