Перейти к содержимому

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:

from functools import lru_cache @lru_cache def count_vowels(sentence): sentence = sentence.casefold() return sum(sentence.count(vowel) for vowel in 'aeiou') 

Если для параметра 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 для статического веб-контента:

from functools import lru_cache @lru_cache(maxsize=32) def get_pep(num): 'Retrieve text of a Python Enhancement Proposal' resource = 'http://www.python.org/dev/peps/pep-%04d/' % num try: with urllib.request.urlopen(resource) as s: return s.read() except urllib.error.HTTPError: return 'Not Found' >>> for n in 8, 290, 308, 320, 8, 218, 320, 279, 289, 320, 9991: . pep = get_pep(n) . print(n, len(pep)) >>> get_pep.cache_info() # CacheInfo(hits=3, misses=8, maxsize=32, currsize=8) 

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

from functools import lru_cache @lru_cache(maxsize=None) def fib(n): if n  2: return n return fib(n-1) + fib(n-2) >>> [fib(n) for n in range(16)] # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610] >>> fib.cache_info() # CacheInfo(hits=28, misses=16, maxsize=None, currsize=16) 

Изменено в версии 3.8: Добавлена ​​опция user_function .

Новое в версии 3.9: Добавлен метод .cache_parameters() .

  • ОБЗОРНАЯ СТРАНИЦА РАЗДЕЛА
  • Способы использования модуля functools
  • Декоратор @cached_property модуля functools
  • Функция cmp_to_key() модуля functools
  • Декоратор @cache() модуля functools, кеширующий декоратор
  • Декоратор @lru_cache() модуля functools
  • Декоратор @total_ordering модуля functools
  • Функция partial() модуля functools
  • Класс partialmethod() модуля functools
  • Функция reduce() модуля functools
  • Декоратор @singledispatch модуля functools
  • Декоратор @singledispatchmethod модуля functools
  • Декоратор @update_wrapper() модуля functools
  • Декоратор @wraps() модуля functools

Как работать с кэшированием данных в Python

Узнайте, как оптимизировать работу Python-приложений с помощью кэширования данных, используя встроенный декоратор и внешние хранилища.

Data caching illustrated with a computer and a cache box.

Алексей Кодов
Автор статьи
23 июня 2023 в 18:57

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

Что такое кэширование данных?

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

Встроенный декоратор functools.lru_cache

Python предоставляет встроенный декоратор functools.lru_cache , который позволяет легко кэшировать результаты функций. LRU (Least Recently Used) кэш — это кэш с ограниченным размером, который автоматически удаляет наименее недавно использованные элементы при необходимости освободить место для новых данных.

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

import functools @functools.lru_cache(maxsize=100) def expensive_function(arg1, arg2): # Здесь выполняется какая-то дорогостоящая операция result = arg1 + arg2 return result # Вызов функции result = expensive_function(1, 2)

В этом примере результаты expensive_function будут кэшироваться и сохраняться в LRU-кэше размером до 100 элементов.

Python-разработчик: новая работа через 9 месяцев
Получится, даже если у вас нет опыта в IT

Кэширование с использованием внешних хранилищ

В некоторых случаях может потребоваться кэширование данных с использованием внешних хранилищ, таких как Redis или Memcached. Это особенно актуально при работе с веб-приложениями и распределенными системами.

Пример кэширования данных с использованием Redis:

import redis cache = redis.Redis(host='localhost', port=6379) def cache_data(key, data, ttl=3600): cache.setex(key, ttl, data) def get_cached_data(key): return cache.get(key) # Кэширование данных cache_data('my_key', 'my_data') # Получение данных из кэша result = get_cached_data('my_key')

В этом примере мы используем библиотеку redis для работы с кэшем Redis. Функции cache_data и get_cached_data позволяют сохранять и получать данные из кэша соответственно.

Заключение

Кэширование данных является полезным инструментом для оптимизации работы приложений и снижения нагрузки на серверы. В Python существует несколько способов реализации кэширования, включая встроенный декоратор functools.lru_cache и использование внешних хранилищ, таких как Redis или Memcached. Экспериментируйте с различными подходами и выбирайте тот, который наиболее подходит для вашего проекта. ��

Реализация кеша с ограничением по числу элементов на Python — решения: простое и посложнее

Предположим, что у нас есть необходимость иметь некий сервис, который бы отдавал нам по запросу какую-либо информацию, и отдавал как можно быстрее. Что для этого делает любой нормальный человек? Налаживает кэширование наиболее часто запрашиваемых данных. При этом, если хоть немного задуматься о перспективе, то размеры кэша необходимо ограничивать.
Для простоты реализации в случае Питона сделаем ограничение по числу элементов в кэше (здесь предполагается, что данные более-менее однородны по размеру, а также учитывается специфика, что определить объём памяти, реально занимаемый каким-либо Питоновским объектом — весьма нетривиальная задача, кому интересно, пусть пожалует сюда), а для того, чтобы кэш содержал как можно более часто используемую информацию — построим его по принципу least recently used, т.е. чем более давно запрашивали кусочек информации, тем больше у него шансов «вылететь» из кэша.

О двух решениях (попроще и посложнее) я и расскажу в этой статье.

Философское отступление

Я обратил внимание, что зачастую написанный на Питоне код не блещет оптимизациями по потреблению памяти или по скорости (здесь можно вскользь заметить, что это не только вина программистов-пользователей языка, хорошего инструментария просто нет, или по крайней мере я о таком не знаю (да, я в курсе про cProfile, но он помогает далеко не всегда, например, в нём нет attach-detach; искать же утечки памяти — вообще занятие не для слабонервных, вот если pympler допилят. ), если кто подскажет — буду благодарен). В основном это даже правильно, так как обычно Питон применяют в случае, когда время исполнения программы или потребление памяти не критично, зато критично время написания кода и простота его дальнейшей поддержки, поэтому очевидное, пусть и менее экономичное решение будет удобнее.
Хотя вышесказанное совсем не означает, что любой код на Питоне нужно писать «абы как». Кроме того, иногда производительность и потребление памяти могут стать критичными, даже если код крутится на серверной машине с хорошим процессором и тоннами памяти.
Как раз такой случай (нехватка CPU time) и подвиг меня на исследование этого вопроса и замену одного кэширующего класса другим.

Простое решение

Часто говорят — «простое решение — самое верное», хотя в случае с программированием это далеко не всегда так, не правда ли? Однако, если есть задача обеспечения кэша, но нет особых требований к скорости (например, то, что использует этот кэш, само по себе очень медленное, и нет смысла вкладываться в сложную реализацию), то можно обойтись и тривиальными решениями.

Реализация

Итак, относительно простое решение:

import weakref class SimpleCache: def __init__(self, cacheSize): self.cache = weakref.WeakValueDictionary() self.cache_LRU = [] self.cacheSize = cacheSize def __getitem__(self, key): # no exception catching - let calling code handle them if any value = self.cache[key] return self.touchCache(key, value) def __setitem__(self, key, value): self.touchCache(key, value) def touchCache(self, key, value): self.cache[key] = value if value in self.cache_LRU: self.cache_LRU.remove(value) self.cache_LRU = self.cache_LRU[-(self.cacheSize-1):] + [ value ] return value 
Использование

Работать с таким кэшем после создания можно, как с обычным dict-ом, но при чтении/записи элемента он будет помещён в конец очереди, а за счёт комбинации WeakValueDictionary() и списка, который хранит не более cacheSize элементов, мы получаем ограничение по количеству сохранённых данных.
Таким образом, после исполнения куска кода

cache = SimpleCache(2) cache[1] = 'a' cache[2] = 'b' cache[3] = 'c'

в кэше останутся только записи ‘b’ и ‘c’ (‘a’, как самая старая, будет вытеснена).

Преимущества и недостатки

Преимущество — относительная простота (около 20 строк кода, после прочтения документации по WeakValueDictionary принцип работы становится понятен).
Недостаток — скорость работы, т.к. при каждом «касании» кэша приходится пересоздавать список целиком, удаляя из произвольного его места элемент (на самом деле там происходит аж целая куча длинных операций по работе со списком, так, «if value in self.cache_LRU» — линейный поиск по списку, потом .remove() — ещё раз линейный поиск, потом ещё берётся срез — т.е. создаётся почти полная копия списка). Словом, относительно долго.

Сложное решение

Теперь вдумаемся — а как можно ускорить такой класс? Очевидно, основные проблемы у нас именно с поддержанием cache_LRU в актуальном состоянии — как я уже говорил, поиск по нему, последующее удаление элемента и пересоздание — дорогие операции. Тут нам на помощь придёт такая вещь, как двунаправленный связный список — он обеспечит нам поддержку «последний использованный — в конце», ну а WeakValueDictionary() поможет с быстрым поиском по этому списку (поиск по словарю идёт куда быстрее линейного перебора, поскольку внутри Питоновские словари реализуют что-то вроде бинарных деревьев по хешам ключей (тут Остапа понесло — могу честно сказать, что в исходники не смотрел, поэтому только предполагаю, как именно устроен поиск по словарю)

Реализация

Для начала создаём класс — элемент списка, в который будем оборачивать хранимые данные:

 class Element(object): __slots__ = ['prev', 'next', 'value', '__init__', '__weakref__'] def __init__(self, value): self.prev, self.next, self.value = None, None, value

Тут надо заметить использование такой штуки, как __slots__, позволяющей существенно сэкономить память и немного — производительность по сравнению с такой же реализацией класса, но без этого атрибута (что это такое и с чем его едят — в официальной документации).
Теперь создаём класс кэша (класс элемента засунем внутрь «во избежание»):

import weakref class FastCache: class Element(object): __slots__ = ['prev', 'next', 'value', '__init__', '__weakref__'] def __init__(self, value): self.prev, self.next, self.value = None, None, value def __init__(self, maxCount): self.dict = weakref.WeakValueDictionary() self.head = None self.tail = None self.count = 0 self.maxCount = maxCount def _removeElement(self, element): prev, next = element.prev, element.next if prev: assert prev.next == element prev.next = next elif self.head == element: self.head = next if next: assert next.prev == element next.prev = prev elif self.tail == element: self.tail = prev element.prev, element.next = None, None assert self.count >= 1 self.count -= 1 def _appendElement(self, element): if element is None: return element.prev, element.next = self.tail, None if self.head is None: self.head = element if self.tail is not None: self.tail.next = element self.tail = element self.count += 1 def get(self, key, *arg): element = self.dict.get(key, None) if element: self._removeElement(element) self._appendElement(element) return element.value elif len(*arg): return arg[0] else: raise KeyError("'%s' is not found in the dictionary", str(key)) def __len__(self): return len(self.dict) def __getitem__(self, key): element = self.dict[key] # At this point the element is not None self._removeElement(element) self._appendElement(element) return element.value def __setitem__(self, key, value): try: element = self.dict[key] self._removeElement(element) except KeyError: if self.count == self.maxCount: self._removeElement(self.head) element = FastCache.Element(value) self._appendElement(element) self.dict[key] = element def __del__(self): while self.head: self._removeElement(self.head)
  • реализация метода get() — слегка отличается от стандартной для словарей, т.к. если не задано значение по умолчанию, а ключ отсутствует, то выкидывает исключение (работает так же, как cache[key]), а если есть значение по умолчанию, то возвращает его
  • наличие метода __del__ обязательно, в противном случае получаем (или можем получить) утечку всего кэша — ведь все элементы списка друг на друга ссылаются, значит, их счётчики ссылок никогда не обнулятся без посторонней помощи; кстати, как выяснилось, встроенный (по крайней мере в 2.6) garbage collector хоть и вроде как собирает простейшие циклы ссылок, но с этим списком не справляется
  • при желании можно слегка поднять производительность, выкинув assert’ы
Использование

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

Ещё одно простое решение с хорошей производительностью

Спасибо seriyPS за подсказку!
Вырисовалось ещё одно решение, которое выглядит так же просто, как первое (если даже не проще) и при этом работает почти так же быстро, как сложное. Итак, реализация:

from collections import OrderedDict class ODCache(OrderedDict): def __init__(self, cacheSize): self.cacheSize = cacheSize OrderedDict.__init__(self) def __getitem__(self, key): value = OrderedDict.__getitem__(self, key) return self._touchCache(key, value) def __setitem__(self, key, value): self._touchCache(key, value) def _touchCache(self, key, value): try: OrderedDict.__delitem__(self, key) except KeyError: pass OrderedDict.__setitem__(self, key, value) toDel = len(self) - self.cacheSize if toDel > 0: for k in OrderedDict.keys(self)[:toDel]: OrderedDict.__delitem__(self, k) return value 
Ещё одно решение — хорошая производительность

Решение из комментариев (по использованным мной тестам — лидер по скорости), спасибо tbd:

class ListDictBasedCache(object): __slots__ = ['__key2value', '__maxCount', '__weights'] def __init__(self, maxCount): self.__maxCount = maxCount self.__key2value = <># key->value self.__weights = []# keys ordered in LRU def __updateWeight(self, key): try: self.__weights.remove(key) except ValueError: pass self.__weights.append(key)# add key to end if len(self.__weights) > self.__maxCount: _key = self.__weights.pop(0)# remove first key self.__key2value.pop(_key) def __getitem__(self, key): try: value = self.__key2value[key] self.__updateWeight(key) return value except KeyError: raise KeyError("key %s not found" % key) def __setitem__(self, key, value): self.__key2value[key] = value self.__updateWeight(key) def __str__(self): return str(self.__key2value)
Сравнение

Голословные утверждения — это одно, а беспристрастные цифры — совсем другое. Поэтому я не призываю верить мне на слово, наоборот — измерим эти классы!
Тут нам на помощь приходит встроенный в Питон модуль timeit:

class StubClass: def __init__(self, something): self.something = something def testCache(cacheClass, cacheSize, repeat): d = cacheClass(cacheSize) for i in xrange(repeat * cacheSize): d[i] = StubClass(i) import random class testCacheReadGen: def __init__(self, cacheClass, cacheSize): d = cacheClass(cacheSize) for i in xrange(cacheSize): d[i] = StubClass(i) self.d = d self.s = cacheSize def __call__(self, repeat): cacheSize, d = self.s, self.d for i in xrange(cacheSize * repeat): tmp = d[random.randint(0, cacheSize-1)] def minMaxAvg(lst): return min(lst), max(lst), 1.0 * sum(lst) / len(lst) import timeit def testAllCaches(classes, cacheSize, repeats): templ = '%s: min %.5f, max %.5f, avg %.5f' genmsg = lambda cls, res: templ % ((cls.__name__.ljust(20),) + tuple(minMaxAvg(res))) for cls in classes: t = timeit.Timer(lambda: testCache(cls, cacheSize, repeats[0])) print genmsg(cls, t.repeat(*repeats[1:])) def testAllCachesRead(classes, cacheSize, repeats): templ = '%s: min %.5f, max %.5f, avg %.5f' genmsg = lambda cls, res: templ % ((cls.__name__.ljust(20),) + tuple(minMaxAvg(res))) for cls in classes: tst = testCacheReadGen(cls, cacheSize) t = timeit.Timer(lambda: tst(repeats[0])) print genmsg(cls, t.repeat(*repeats[1:])) if __name__ == '__main__': print 'write' testAllCaches((SimpleCache, FastCache, ODCache, ListDictBasedCache), 100, (100, 3, 3)) print 'read' testAllCachesRead((SimpleCache, FastCache, ODCache, ListDictBasedCache), 100, (100, 3, 3))

Результаты запуска на моей машине (Intel Core i5 2540M 2.6GHz, Win7 64-bit, ActivePython 2.7.2 x64-bit):

write SimpleCache : min 9.36119, max 9.49077, avg 9.42536 FastCache : min 0.39449, max 0.41835, avg 0.40880 ODCache : min 0.79536, max 0.82727, avg 0.81482 ListDictBasedCache : min 0.25135, max 0.27334, avg 0.26000 read SimpleCache : min 9.61617, max 9.73143, avg 9.66337 FastCache : min 0.19294, max 0.21941, avg 0.20552 ODCache : min 0.22270, max 0.25816, avg 0.23911 ListDictBasedCache : min 0.16475, max 0.17725, avg 0.16911

Разница между простым и сложным решениями довольно ощутима — порядка 20 раз! Решение на OrderedDict чуть отстаёт в плане производительности, но совсем незначительно. Для дальнейших выводов нужно делать более сложные измерения, отражающие особенность задачи кэша — быстрый доступ к произвольным кусочкам информации, а не некоторый линейный, как использовался выше.

По потреблению памяти — запускаем предыдущий скрипт с другой секцией main и смотрим потребление памяти интерпретатором Питона через диспетчер задач:

if __name__ == '__main__': import time print 'measure me - no cache' try: while True: time.sleep(10) except: # let user interrupt this with Ctrl-C pass testCache(FastCache, 1000, 1) print 'measure me - cached' while True: time.sleep(10) exit()

Результаты измерений — в таблице:

Класс кэша до создания кэша после создания кэша потребление кэша
SimpleCache 4228K 4768K 540K
FastCache 4232K 4636K 404K
ODCache 4496K 4936K 440K
ListDictBasedCache 4500K 4880K 380K

Как видим — сложное решение не только заметно быстрее (в ~20 раз) добавляет элементы, но и потребляет немного меньше памяти, чем простое.

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

@lru_cache и Рекурсия, как работает

Сдаю информатику, 19 задание егэ. Никак не могу понять по какому алгоритму работает рекурсия и как она конкретно записывается в кеш. ИЗ КАКОЙ СТУПЕНИ РЕКУРСИИ ВЫВОДИТСЯ ‘B1’ (к примеру если создам функцию в которой напишу две строки — return 1 и return 2, то после вызова выведется 1, тк почему то только первый обьявленный return возвращает). Там я во списковых включениях вызывается та же функция что и в описании, выходит рекурсия. Как вообще работает код? и простыми словами как @lru_cache записывает кеш? что за значение None? я это гуглю два дня уже, и по меркам егэ это непростительно продолжительное время введите сюда описание изображения Как я вижу алгоритм типа..

 from functools import lru_cache # ПО УСЛОВИЮ ЗАДАЧИ ДВА ИГРОКА И ДВЕ КУЧИ КАМНЕЙ # ЗА ОДИН ХОД МОЖНО ДОБАВИТЬ 1 КАМЕНЬ В ОДНУ ИЗ КУЧ ИЛИ УМНОЖИТЬ ОДНУ ИЗ КУЧ ВДВОЕ # В НАЧАЛЬНЫЙ МОМЕНТ В ПЕРВОЙ КУЧЕ 10, ВО ВТОРОЙ S (1 = 62: # вот тут и начинаются все приколы return 'WIN' if any( game(m) == 'WIN' for m in move(h) ): return 'B1' # а вот тут они заканчиваются . (ПОБЕДА ПЕРВОГО ИГРОКА) for S in range(1, 52): if game( (1, S) ) is not None: print('<>: <>'.format(S, game( (10, S) ) )) print(game.cache_info()) # Пытался что гуглить буду, но ниче не нашёл понятного 

Отслеживать
Ленд Вульпбп
задан 28 дек 2022 в 16:30
Ленд Вульпбп Ленд Вульпбп
25 7 7 бронзовых знаков
Код приводят форматированным ТЕКСТОМ
28 дек 2022 в 16:50
Код текстом в вопрос, только мат из комментариев уберите, а то ещё хуже всё будет.
28 дек 2022 в 16:58
@CrazyElf приношу дикие извинения, я масля на форуме, в крайнем случае обратился.
28 дек 2022 в 17:10

1 ответ 1

Сортировка: Сброс на вариант по умолчанию

Непонятно, какой именно None вам непонятен. Если как параметр декоратора lru_cache , то это просто означает, что размер кэша не ограничен. Если задать кэшу ограничение на кол-во хранимых элементов, то при необходимости положить в кэш ещё одно значение и заполненности кэша до этого указанного значения LRU cache посмотрит у себя статистику — какой из элементов кэша запрашивался наиболее давно — и выкинет этот элемент, чтобы добавить новый. Таким образом, размер кэша никогда не будет больше, чем указанный.

А если вас интересует None как результат работы кэшируемой функции, то None будет возвращён, если в вашей функции не сработают оба if и значит не сработает ни один из return . В питоне если функция закончилась, а return ни один не выполнился, то считается, что как будто выполнился return None . Такая договорённость. Ничего не вернувшая функция считается вернувшей None .

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

Метод cache_info показывает статистику:

  • сколько раз кэш сработал удачно (в кэше нашлись аргументы и он вернул значение не выполняя функцию)
  • сколько раз кэш «промахнулся» (пришлось таки вызвать функцию, чтобы получить результат и запомнить его потом в кэше)
  • какой был задан максимальный размер для кэша функции
  • сколько элементов на данный момент хранится в кэше декоратора этой функции

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

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

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

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