Как реализовать кэширование абстрактно?
Что проверяют
Понимаете ли вы кэш как обёртку над функцией или хранилищем, которую можно подменить, и знаете ли, чем готовые средства Python ограничены. Следом часто спрашивают, как добавить TTL к lru_cache или почему он течёт на методах класса.
Как отвечать
В Python абстракция кэша естественно ложится на декоратор: функция не знает, что её результат запоминается. Опишите слои:
functools.lru_cache: мемоизация в памяти процесса,maxsizeпо умолчанию 128,maxsize=Noneснимает лимит. С Python 3.9 есть короткийfunctools.cache, он безлимитный. Естьcache_info()иcache_clear(), а TTL нет вовсе;- ключ: строится из аргументов, поэтому они обязаны быть хешируемыми. Список или словарь в аргументах даст
TypeError; functools.cached_property: одно вычисление на экземпляр, результат хранится в его__dict__;cachetools:TTLCache,LRUCache,LFUCacheи декораторcachedс явным объектом кэша и замком;- внешний кэш: Redis через
redis-pyсSET key value EX 300, когда процессов несколько (воркеры gunicorn, несколько подов) и нужен общий кэш. В Django то же даётdjango.core.cacheс подменяемым бэкендом.
Хороший каркас ответа: интерфейс get/set/delete с TTL, реализация в памяти для тестов и Redis для прода, декоратор, который собирает ключ из имени функции и аргументов. Для сериализации в Redis берите JSON или msgpack. pickle из общего хранилища опасен, если туда может писать кто-то ещё.
Где спотыкаются
Ставят @lru_cache на метод. В ключ попадает self, кэш держит ссылки на экземпляры, и сборщик мусора их не освобождает.
Вешают lru_cache на async def. Кэшируется объект корутины, а не результат, и повторный await того же объекта падает с RuntimeError.
Считают lru_cache общим для приложения. Каждый воркер gunicorn держит свою копию, и инвалидация в одном процессе не видна в остальных.