Мастерство стандартной библиотеки python: functools.cached_property
В python есть великолепный модуль functools
, в котором находится, пожалуй, моя самая любимая штука во всем python - декораторы lru_cache
и cached_property
.
lru_cache
позволяет кешировать (иногда еще говорят мемоизировать) результаты работы декорируемых функций - это чертовски ускоряет повторное (или рекурсивное) исполение CPU bound функций, как впрочем и для IO bound функций он бывает полезен.cached_property
позволяет кешировать исполнение метода класса, но только если у него дополнительных аргументов, кроме self.
Вместе они позволяют покрыть большинство хотелок по кешированию чего-либо прямо в memory питоньего процесса.
Однако, есть у них как неочивидные фишки, так и некоторые тонкости, о которых сейчас и поговорим.
Начнем с декоратора cached_property
Использовать его очень просто:
Строчка ‘actually running…’ напечаталась только раз. Результат сохранили и переиспользовали. Сохранили именно внутри объекта. То есть:
Умрет объект - умрет и закешированное значение. Это важно. Мы еще к этому вернемся, когда будем говорить о
lru_cache
.
Забавная фишка, про которую мало кто знает: можно удалить атрибут - и кэш сбросится. Следующий вызов будет честным вызовом функции, результат снова закешируется.
Вторая забавная фишка, про которую мало кто знает: вы сами можете записывать в атрибут все, что пожелаете, и ничего не сломается.
Вывод: cached_property
- это не какая-то магия, а очень простая штука: “Если в объекте нет атрибута - то cached_property позовет функцию и запишет ее вывод в атрибут.”
Прикольно, но что если нам все таки нужно звать функцию/метод с аргументом?
Напрямую воспользоваться cached_property
не получится. По идее, для этого и есть lru_cache, но он заслуживает отдельного поста. Пока рассмотрим менее очевидный способ применения cached_property
.
Для примера я взял фрагмент production кода, который я недавно написал. Тут и type hinting, и type alias, а еще тут используется модный pydantic. Вы ведь про него слышали, да?
Итак - есть объект SomeState, представляющий текущее состояние неких GroupState, которые просто себе лежат в словарике groups по ключу gid. Мы хотим искать группы в стейте не только по gid (что легко и быстро - обращение по индексу в словаре), но и по mid - а mid лежат внутри объектов GroupState неким образом. Но перебирать на каждый запрос SomeState.find_gid(mid)
все группы и все mid в них не очень быстро, хотелось бы это как-то закешировать…
Задекорировать метод find_gid с помощью cached_property
нельзя из-за аргумента mid. По идее, мы можем просто задекорировать его с помощью lru_cache
.. но пока не будем;)
А будем мы делать вот так:
Мы оставляем метод SomeState.find_gid
не закешированным, но все, что он делает - это lookup в SomeState._mid_index
, а это уже закешированный дикт, в котором лежат все известные нам mid и соответствующие им gid.
Если стейты меняются не часто (каждый новый стейт - это новый объект, с пустым кешем), а запросы к SomeState.find_gid
происходят часто - то кеш будет очень даже эффективен.
Тем более, мы не делаем O(n) поиск по всему дикту на каждый запрос, а один раз тратим O(n) на весь стейт сразу.
Смущены загадочным термином O(n)? Вам сюда.
Если бы мы просто задекорировали метод find_gid с помощью lru_cache
- то да, мы получили бы ускорение для повторных запросов с тем же mid, но каждое построение кэша SomeState.find_gid(mid)
стоило бы нам O(n) - ведь в худшем случае нам нужно пробежать по всем группам, чтобы найти mid. И так для каждого запрашиваемого mid.
А еще появилась бы необходимость сделать SomeState хешируемым объектом (wtf?!), но об этом в следующий раз.
Ну а cached_property
обойдется нам только один раз в O(n) и закеширует все mid сразу, без регистрации и смс.
Круто? Очень круто.
Получается, в каком-то смысле, cached_property
может кешировать методы с аргументом. Но только в определенных ситуациях.
Думайте об этом подходе, как об индексе в БД. Только перестраивается он не при записи, а при первой попытке чтения. Lazy индекс, получается. Ну и мы тоже немного lazy - добились желаемого, не изучая lru_cache.
Что еще почитать?
Заинтересовались type hinting’ами и type alias’ами?
Загадочный O(n), он жe Big O или Big O notation - по быстрому тут, но вообще Вам нужна книга “Грокаем алгоритмы”, глава 1.