Lru cache python что это
Перейти к содержимому

Lru cache python что это

Декоратор @lru_cache() модуля functools в Python.

Декоратор @lru_cache() модуля functools оборачивает функцию с переданными в нее аргументами и запоминает возвращаемый результат соответствующий этим аргументам. Такое поведение может сэкономить время и ресурсы, когда дорогая или связанная с вводом/выводом функция периодически вызывается с одинаковыми аргументами.

Позиционные и ключевые аргументы, переданные в функцию должны быть хешируемыми, т.к. для кэширования результатов декоратор lru_cache() использует словарь.

Различные аргументы можно рассматривать как отдельные вызовы с отдельными записями в кэше. Например f(a=1, b=2) и f(b=2, a=1) различаются по порядку ключевых аргументов и могут иметь две отдельные записи в кэше.

Если указана user_function , то она должна быть вызываемой. Это позволяет применять декоратор @lru_cache непосредственно к пользовательской функции, оставляя значение maxsize по умолчанию равным 128:

Если для параметра maxsize установлено значение None , функция LRU декоратора отключена и кэш может расти без ограничений. Функция LRU работает лучше всего, когда maxsize является степенью двойки.

Если для typed задано значение True , то аргументы функций разных типов будут кэшироваться отдельно. Например f(3) и f(3.0) будут рассматриваться как отдельные вызовы с разными результатами. Если введено typed=False , ТО реализация обычно будет рассматривать их как эквивалентные вызовы и кэшировать только один результат. (Некоторые типы, такие как str и int , могут кэшироваться отдельно, даже если передано False )

Обратите внимание: специфичность типа применяется только к непосредственным аргументам функции, а не к их содержимому. Скалярные аргументы Decimal(42) и Fraction(42) обрабатываются как отдельные вызовы с разными результатами. Напротив, аргументы кортежа ('answer', Decimal(42)) и ('answer', Fraction(42)) обрабатываются как эквивалентные.

Обернутая функция в этот декоратор будет иметь метод .cache_parameters() (Новое в версии 3.9), которая возвращает новый dict , показывающий значения maxsize и typed . Этот метод служит только для информационных целей. Изменение значений не имеет никакого эффекта.

Чтобы помочь измерить эффективность кэша и настроить параметр maxsize , декорированная функция получает метод .cache_info() , который возвращает именованный кортеж, показывающий hits , misses , maxsize и currsize . В многопоточной среде hits и misses являются приблизительными.

Декоратор также предоставляет метод .cache_clear() для очистки или аннулирования кэша.

Исходная базовая функция доступна через атрибут __wrapped__ , что полезно для самоанализа, для обхода кеша или для преобразования функции в другой кеш.

Кэш LRU работает лучше всего, т.к. алгоритм кэширования LRU, при наличии установленного параметра maxsize , в первую очередь вытесняет неиспользованные дольше всех кэшированные значения. Ограничение размера кеша гарантирует, что кеш не будет расти без привязки к длительным процессам, таким как веб-серверы.

В общем случае кэш LRU следует использовать только тогда, когда вы хотите повторно использовать ранее вычисленные значения. Нет смысла кэшировать функции, которые должны создавать различные изменяемые объекты при каждом вызове или нечистые функции, такие как time.time() или random.random() .

Примеры использования:

Пример кэша LRU для статического веб-контента:

Пример эффективного вычисления чисел Фибоначчи с использованием кэша для реализации техники динамического программирования:

functools — Higher-order functions and operations on callable objects¶

The functools module is for higher-order functions: functions that act on or return other functions. In general, any callable object can be treated as a function for the purposes of this module.

The functools module defines the following functions:

@ functools. cache ( user_function ) ¶

Simple lightweight unbounded function cache. Sometimes called “memoize”.

Returns the same as lru_cache(maxsize=None) , creating a thin wrapper around a dictionary lookup for the function arguments. Because it never needs to evict old values, this is smaller and faster than lru_cache() with a size limit.

New in version 3.9.

Transform a method of a class into a property whose value is computed once and then cached as a normal attribute for the life of the instance. Similar to property() , with the addition of caching. Useful for expensive computed properties of instances that are otherwise effectively immutable.

The mechanics of cached_property() are somewhat different from property() . A regular property blocks attribute writes unless a setter is defined. In contrast, a cached_property allows writes.

The cached_property decorator only runs on lookups and only when an attribute of the same name doesn’t exist. When it does run, the cached_property writes to the attribute with the same name. Subsequent attribute reads and writes take precedence over the cached_property method and it works like a normal attribute.

The cached value can be cleared by deleting the attribute. This allows the cached_property method to run again.

Note, this decorator interferes with the operation of PEP 412 key-sharing dictionaries. This means that instance dictionaries can take more space than usual.

Also, this decorator requires that the __dict__ attribute on each instance be a mutable mapping. This means it will not work with some types, such as metaclasses (since the __dict__ attributes on type instances are read-only proxies for the class namespace), and those that specify __slots__ without including __dict__ as one of the defined slots (as such classes don’t provide a __dict__ attribute at all).

If a mutable mapping is not available or if space-efficient key sharing is desired, an effect similar to cached_property() can be achieved by a stacking property() on top of cache() :

New in version 3.8.

Transform an old-style comparison function to a key function . Used with tools that accept key functions (such as sorted() , min() , max() , heapq.nlargest() , heapq.nsmallest() , itertools.groupby() ). This function is primarily used as a transition tool for programs being converted from Python 2 which supported the use of comparison functions.

A comparison function is any callable that accept two arguments, compares them, and returns a negative number for less-than, zero for equality, or a positive number for greater-than. A key function is a callable that accepts one argument and returns another value to be used as the sort key.

For sorting examples and a brief sorting tutorial, see Sorting HOW TO .

New in version 3.2.

Decorator to wrap a function with a memoizing callable that saves up to the maxsize most recent calls. It can save time when an expensive or I/O bound function is periodically called with the same arguments.

Since a dictionary is used to cache results, the positional and keyword arguments to the function must be hashable.

Distinct argument patterns may be considered to be distinct calls with separate cache entries. For example, f(a=1, b=2) and f(b=2, a=1) differ in their keyword argument order and may have two separate cache entries.

If user_function is specified, it must be a callable. This allows the lru_cache decorator to be applied directly to a user function, leaving the maxsize at its default value of 128:

If maxsize is set to None , the LRU feature is disabled and the cache can grow without bound.

If typed is set to true, function arguments of different types will be cached separately. If typed is false, the implementation will usually regard them as equivalent calls and only cache a single result. (Some types such as str and int may be cached separately even when typed is false.)

Note, type specificity applies only to the function’s immediate arguments rather than their contents. The scalar arguments, Decimal(42) and Fraction(42) are be treated as distinct calls with distinct results. In contrast, the tuple arguments (‘answer’, Decimal(42)) and (‘answer’, Fraction(42)) are treated as equivalent.

The wrapped function is instrumented with a cache_parameters() function that returns a new dict showing the values for maxsize and typed. This is for information purposes only. Mutating the values has no effect.

To help measure the effectiveness of the cache and tune the maxsize parameter, the wrapped function is instrumented with a cache_info() function that returns a named tuple showing hits, misses, maxsize and currsize.

The decorator also provides a cache_clear() function for clearing or invalidating the cache.

The original underlying function is accessible through the __wrapped__ attribute. This is useful for introspection, for bypassing the cache, or for rewrapping the function with a different cache.

The cache keeps references to the arguments and return values until they age out of the cache or until the cache is cleared.

An LRU (least recently used) cache works best when the most recent calls are the best predictors of upcoming calls (for example, the most popular articles on a news server tend to change each day). The cache’s size limit assures that the cache does not grow without bound on long-running processes such as web servers.

In general, the LRU cache should only be used when you want to reuse previously computed values. Accordingly, it doesn’t make sense to cache functions with side-effects, functions that need to create distinct mutable objects on each call, or impure functions such as time() or random().

Example of an LRU cache for static web content:

Example of efficiently computing Fibonacci numbers using a cache to implement a dynamic programming technique:

�� Кэширование в Python: алгоритм LRU

�� Кэширование в Python: алгоритм LRU

Эта публикация – незначительно сокращенный перевод статьи Сантьяго Валдаррама Caching in Python Using the LRU Cache Strategy. Переведенный текст также доступен в виде блокнота Jupyter.

Кэширование – один из подходов, который при правильном использовании значительно ускоряет работу и снижает нагрузку на вычислительные ресурсы. В модуле стандартной библиотеки Python functools реализован декоратор @lru_cache , дающий возможность кэшировать вывод функций, используя стратегию Least Recently Used (LRU, «вытеснение давно неиспользуемых»). Это простой, но мощный метод, который позволяет использовать в коде возможности кэширования.

В этом руководстве мы рассмотрим:

  • какие стратегии кэширования доступны и как их реализовать с помощью декораторов;
  • что такое LRU и как работает этот подход;
  • как повысить производительность программы с помощью декоратора @lru_cache ;
  • как расширить функциональность декоратора @lru_cache и прекратить кэширование по истечении определенного интервала времени.

Кэширование в Python: в чем польза

Кэширование – это метод оптимизации хранения данных, при котором операции с данными производятся эффективнее, чем в их источнике.

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

Как поступит программа, если читатель решит сравнить пару статей и станет многократно между ними перемещаться? Без кэширования приложению придется каждый раз получать одно и то же содержимое. В этом случае неэффективно используется и система пользователя, и сервер со статьями, на котором создается дополнительная нагрузка.

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

Реализация кэширования в Python посредством словаря

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

Примечание. Для запуска этого примера у вас должна быть установлена библиотека requests :

Хотя вызов get_article() выполняется дважды, статья с сервера загружается лишь один раз. После первого доступа к статье мы помещаем ее URL и содержимое в словарь cache . Во второй раз код не требует повторного получения элемента с сервера.

Стратегии кэширования

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

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

Стратегия Какую запись удаляем Эти записи чаще других используются повторно
First-In/First-Out (FIFO) Самая старая Новые
Last-In/First-Out (LIFO) Самая недавняя Старые
Least Recently Used (LRU) Использовалась наиболее давно Недавно прочитанные
Most Recently Used (MRU) Использовалась последней Прочитанные первыми
Least Frequently Used (LFU) Использовалась наиболее редко Использовались часто

Погружаемся в идею LRU-кэширования

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

На следующем рисунке показано представление кэша после того, как пользователь запросил статью из сети.

Процесс заполнения LRU-кэша, шаг 1Процесс заполнения LRU-кэша, шаг 1

Статья сохраняется в последнем слоте кэша перед тем, как будет передана пользователю. На следующем рисунке показано, что происходит, когда пользователь запрашивает следующую статью.

Процесс заполнения LRU-кэша, шаг 2Процесс заполнения LRU-кэша, шаг 2

Вторая статья занимает последний слот, перемещая первую статью вниз по списку.

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

Заглядываем за кулисы кэша LRU

Один из способов реализовать кэш LRU в Python – использовать комбинацию двусвязного списка и хеш-таблицы. Головной элемент двусвязного списка указывает на последнюю запрошенную запись, а хвостовой – на наиболее давно использовавшуюся.

На рисунке ниже показана возможная структура реализации кэша LRU.

Схема реализации кэшированияСхема реализации кэширования

Используя хеш-таблицу, мы обеспечиваем доступ к каждому элементу в кэше, сопоставляя каждую запись с определенным местом в двусвязном списке. При этом доступ к недавно использовавшемуся элементу и обновление кэша – это операции, выполняемые за константное время (то есть с временной сложностью алгоритма �� ( 1 ) .

Начиная с версии 3.2, для реализации стратегии LRU Python включает декоратор @lru_cache .

Использование @lru_cache для реализации кэша LRU в Python

Декоратор @lru_cache за кулисами использует словарь. Результат выполнения функции кэшируется под ключом, соответствующим вызову функции и предоставленным аргументам. То есть чтобы декоратор работал, аргументы должны быть хешируемыми.

Наглядное представление алгоритма: перепрыгиваем ступеньки

Представим, что мы хотим определить число способов, которыми можем достичь определенной ступеньки на лестнице. Сколько есть способов, например, добраться до четвертой ступеньки, если мы можем переступить-перепрыгнуть 1, 2, 3 (но не более) ступеньки? На рисунке ниже представлены соответствующие комбинации.

�� Кэширование в Python: алгоритм LRU

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

�� Кэширование в Python: алгоритм LRU

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

Опишем программно рекурсивное решение в точности, как мы его сейчас видим:

Код работает для 4 ступенек. Давайте проверим, как он подсчитает число вариантов для лестницы из 30 ступенек.

Получилось свыше 53 млн. комбинаций. Однако когда мы искали решение для тридцатой ступеньки, сценарий мог длиться довольно долго.

Засекаем время выполнения программного кода

Измерим, как долго длится выполнение кода.

Для этого мы можем использовать модуль Python timeit или соответствующую команду в блокноте Jupyter.

Количество секунд зависит от характеристик используемого компьютера. В моей системе расчет занял 3 секунды, что довольно медленно для всего тридцати ступенек. Это решение можно значительно улучшить c помощью мемоизации.

Использование мемоизации для улучшения решения

Наша рекурсивная реализация решает проблему, разбивая ее на более мелкие шаги, которые дополняют друг друга. На следующем рисунке показано дерево для семи ступенек, в котором каждый узел представляет определенный вызов steps_to() :

Дерево вызовов функции step_to()Дерево вызовов функции step_to()

Можно заметить, что алгоритму приходится вызывать steps_to() с одним и тем же аргументом несколько раз. Например, steps_to(5) вычисляется два раза, steps_to(4) – четыре раза, steps_to(3) – семь раз и т. д. Вызов одной и той же функции несколько раз запускает вычисления, в которых нет необходимости – результат всегда один и тот же.

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

Импортируем декоратор из модуля functools и применим к основной функции.

От единиц секунд к десяткам наносекунд – потрясающее улучшение, обязанное тем, что за кулисами декоратор @lru_cache сохраняет результаты вызова steps_to() для каждого уникального входного значения.

Другие возможности @lru_cache

Подключив декоратор @lru_cache , мы сохраняем каждый вызов и ответ в памяти для последующего доступа, если они потребуются снова. Но сколько таких комбинаций мы можем сохранить, пока не иссякнет память?

У декоратора @lru_cache есть атрибут maxsize , определяющий максимальное количество записей до того, как кэш начнет удалять старые элементы. По умолчанию maxsize равен 128. Если мы присвоим maxsize значение None , то кэш будет расти без всякого удаления записей. Это может стать проблемой, если мы храним в памяти слишком много различных вызовов.

Применим @lru_cache с использованием атрибута maxsize и добавим вызов метода cache_info() :

Мы можем использовать информацию, возвращаемую cache_info() , чтобы понять, как работает кэш, и настроить его, чтобы найти подходящий баланс между скоростью работы и объемом памяти:

  • hits=52 – количество вызовов, которые @lru_cache вернул непосредственно из памяти, поскольку они присутствовали в кэше;
  • misses=30 – количество вызовов, которые взяты не из памяти, а были вычислены (в случае нашей задачи это каждая новая ступень);
  • maxsize=16 – это размер кэша, который мы определили, передав его декоратору;
  • currsize=16 – текущий размер кэша, в этом случае кэш заполнен.

Добавление срока действия кэша

Перейдем от учебного примера к более реалистичному. Представьте, что мы хотим отслеживать появление на ресурсе Real Python новых статей, содержащих в заголовке слово python – выводить название, скачивать статью и отображать ее объем (число символов).

Real Python предоставляет протокол Atom, так что мы можем использовать библиотеку feedparser для анализа канала и библиотеку requests для загрузки содержимого статьи, как мы это делали раньше.

Скрипт будет работать непрерывно, пока мы не остановим его, нажав [Ctrl + C] в окне терминала (или не прервем выполнение в Jupyter-блокноте).

Код загружает и анализирует xml-файл из RealPython. Далее цикл перебирает первые пять записей в списке. Если слово python является частью заголовка, код печатает заголовок и длину статьи. Затем код «засыпает» на 5 секунд, после чего вновь запускается мониторинг.

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

Мы можем использовать декоратор @lru_cache , однако содержание статьи со временем может измениться. При первой загрузке статьи декоратор сохранит ее содержимое и каждый раз будет возвращать одни и те же данные. Если сообщение обновлено, то сценарий мониторинга никогда об этом не узнает. Чтобы решить эту проблему, мы должны установить срок хранения записей в кэше.

Критерии исключения записей из кэша

Мы можем реализовать описанную идею в новом декораторе, который расширяет @lru_cache . Кэш должен возвращать результат на запрос только, если срок кэширования записи еще не истек – в обратном случае результат должен забираться с сервера. Вот возможная реализация нового декоратора:

Декоратор @timed_lru_cache реализует функциональность для оперирования временем жизни записей в кэше (в секундах) и максимальным размером кэша.

Код оборачивает функцию декоратором @lru_cache . Это позволяет нам использовать уже знакомую функциональность кэширования.

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

Кэширование статей с помощью нового декоратора

Теперь мы можем использовать новый декоратор @timed_lru_cache с функцией monitor() , чтобы предотвратить скачивание с сервера содержимого статьи при каждом новом запросе. Собрав код в одном месте, получим следующий результат:

Обратите внимание, как код печатает сообщение «Получение статьи с сервера . » при первом доступе к соответствующим статьям. После этого, в зависимости от скорости cети, сценарий будет извлекать статьи из кэша несколько раз, прежде чем снова обратится к серверу.

В приведенном примере скрипт пытается получить доступ к статьям каждые 5 секунд, а срок действия кэша истекает раз в минуту.

Заключение

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

В этом уроке мы кратко рассмотрели:

  • какие бывают стратегии кэширования;
  • как работает LRU-кэширование в Python;
  • как использовать декоратор @lru_cache ;
  • как рекурсивный подход в сочетании с кэшированием помогает достаточно быстро решить задачу.

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *