Python. Кэширование вызова метода когда lru_cache не помогает.

Alexandr Sokolov
6 min readSep 12, 2020

--

Сказ о том как я свой кэшируюий декоратор писал.

А начнем мы с определения проблемы и из чего она возникла. Дана функция логгирования, которая пишет лог ошибок обработки наборов неких данных. При этом ошибки в этом наборе идентичны, к примеру имея набор из 100 объектов если в одном, как правило первом, есть ошибка, то она есть во всех остальных, но не факт. Однако функция не просто пишет лог в файл, а отправляет его на систему трекинга ошибок, например sentry.

Вот как выглядит метод

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

Кроме этого было замечено увеличение потребления памяти при генерации кучи репортов.

lru_cache

В коробке с самим Python идет функция lru_cache из модуля functools. LRU (Least Recently Used) — алгоритм при котором в кэше хранятся только самые часто-используемые значения. lru_cache — это декоратор, который инициализирует свое состояние на этапе запуска, а весь кэш хранится все время работы программы. Имеется 2 параметра: max_size определяющий максимальное кол-во элементов в кэше, при достижении которого будет удалено одно значение, которое использовалось реже остальных и typed который по умолчанию False и

If typed is set to true, function arguments of different types will be cached separately. For example, f(3) and f(3.0) will be treated as distinct calls with distinct results.

Что интересно, у меня не вышло проверить это на практике

как видно мы действительно вызываем метод дважды
Все еще работает
Работает…
Что-то пошло не так

Скрин на каждую попытку скорее необходимость, так как опасно говорить, что внутри пайтона что-то не работает, скорее у меня руки из… ну об этом чуть позже.

Больше примеров вы всегда можете найти в документации https://docs.python.org/3/library/functools.html#functools.lru_cache

И все же, параметр typed меня не интересует, однако есть еще 2 фактора препятствующих его использованию в данном случае. 1) все аргументы вызываемого метода обязаны быть хэшируемыми. 2) хэширование. На втором остановимся подробнее.

Вернемся на первый скрин и обратим внимание на строки 23–24. Они говорят о том, что аргумент message может быть инстансом класса Exception, а раз это инстанс, то хэш будет разный.

Но не всегда

Да, я понимаю, что мог либо переписать все вызовы метода send_log так, чтобы всегда передавать строку, однако я специально и добавил это в функцию логгирования, так как устал это повторять. Второй вариант получать объект Exception из sys.exc_info(). И третий вариант добавить еще один декоратор который бы до вызова кэширования переводил сообщение в строку из объекта Exception. Неть :-)

Итак, lru_cache отличный инструмент, который в данном случае мне не подходит. sys.exc_info() хороший вариант, который нужно было сделать раньше, а теперь мне придется менять код всех мест вызова send_log(), а еще кэшировать я хочу только по значению 2-х аргументов и через какое-то время инвалидировать кэш. А значит пишем свои костыли.

Пишем параметризованный декоратор, который будет принимать: 1) объект реализующий кэш 2) ключ 3) время жизни кэша 4) версию.

Интерфейс для базового класса реализующего кэш был позаимствован у django.core.cache.backends.base.BaseCache. Меня интересовало чтобы объект реализовывал методы c.has_key(key, version), c.get(key, version) и set(key, value, timeout, version).

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

Время жизни в секундах должно быть целым числом либо None.

Версия — это скорее побочный эффект, так как я уже использовал кэширование для того же ключа который хотел использовать и тут, хотя позже это оказалось невостребованно. Удивительно, что невостребованно мною же :-)

Перейдем к деталям реализации моих грязных хаков. У каждой функции есть аттрибуты f.__defaults__, f.__kwdefaults__ и f.__code__ (и не только).

__defaults__ — содержит tuple из значений аргументов по умполчанию

__kwdefaults__ — содержит словарь, где ключи это аргументы, а значения это дефолтные значения этих аргументов. Чуть позже объясню разницу.

__code__ — объект с откомпилированным телом функции, хранит имена переменных, как тех что определены в сигнатуре функции, так и те что доступны в теле функции.

Я не помню с какой версии пайтона стало возможным определять имена аргументов как keyword only указав * в аргументах, кажется 3.4. Так вот, если это сделать, то __defaults__ будет пуст, а аргументы после * словарем попадут в __kwdefaults__. В противном случае __kwdefaults__ будет пуст.

Из __code__ мне нужны будут только 2 параметра: .co_varnames содержащий tuple с именами переменных доступных функции и .co_argcount содержащий число — кол-во позиционных аргументов.

Этого будет достаточно для того, чтобы восстановить сигнатуру метода с переданными через *args, **kwargs значениями переменных для этого метода.

Результат на лицо :-)

Замечательно тут то, что это работает и работает хорошо. Однако у нас есть проблема — нельзя декорировать методы классов. Это мне подходит на данный момент.

Ну и добавим само получение ключа и поик по кэшу

Интерфейс который требуется от кеша

И код примера

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

И пишем еще один класс для кэширования, который уже реализует кэширование используя django.core.cache

И декорируем метод send_log

from django.core.cache import cache...@cached(Memcached(cache), key=make_key, timeout=600, version="log")
def send_log(...):

И тут начинается файл

Как видно значение passed_a было утрачено, а значения b и c поменяно местами. Есть проблемы? Будем решать.

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

inspect.Signature._bind

А почитав документацию по модулю inspect, что стоило сделать сразу, находим там функцию signature, которая уже умеет делать все что требуется. И таким образом реализация сразу становится короткой и работающей и даже методы классов теперь можно декорировать :-)

Интересно взглянуть на разницу? А я все равно покажу

Отлично, с этим разобрались, осталось вернуться к методу send_log() и добавить ему return True, так как если он будет возвращать None, то в кэше этого не будет.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Alexandr Sokolov
Alexandr Sokolov

Written by Alexandr Sokolov

Software Engineer. Python Developer.

No responses yet

Write a response