Функции для кэширования функций (еще немного functools)

автор рубрика
Функции для кэширования функций (еще немного functools)

В прошлой статье мы говорили о полезных функциях functools. Сегодня рассмотрим функции кэширования lru_cache, cache, cached_property. Они вам могут пригодиться в тех случаях, когда ваши функции постоянно вычисляет одни и те же значения. Поэтому чтобы избежать повторных вычислений, вы можете добавить результаты вызова функций в кэше.

Функция lru_cache из functools

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

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

Продемонстрируем это на примере. Допустим мы написали рекурсивную функцию подсчета арифметической прогрессии. Вот так она выглядит:

def mysum(n):
    if n == 1:
        return n
    print(f"'{n}'", end=" ")
    return n + mysum(n - 1)

Мы специально добавили функцию print для вывода в стандартный поток вывода текущего значения n. Повызываем эту функцию с разными значениями:

>>> mysum(11)
'11' '10' '9' '8' '7' '6' '5' '4' '3' '2' 66
>>> mysum(11)
'11' '10' '9' '8' '7' '6' '5' '4' '3' '2' 66
>>> mysum(7)
'7' '6' '5' '4' '3' '2' 28
>>> mysum(9)
'9' '8' '7' '6' '5' '4' '3' '2' 45

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

import functools as ftl

@ftl.lru_cache
def mysum(n):
    if n == 1:
        return n
    print(f"'{n}'", end=" ")
    return n + mysum(n - 1)

 >>> mysum(11)
 '11' '10' '9' '8' '7' '6' '5' '4' '3' '2' 66
 >>> mysum(11)
 66
 >>> mysum(9)
 45
 >>> mysum(7)
 28
 >>> mysum(15)
 '15' '14' '13' '12' 120

Поскольку результаты от sum(1) до sum(11) уже найдены и сохранены в кэше, то они из него достаются, поэтому mysum(15) вычисляет вызовы от 15 до 12.

Как работает Least Recently Used (LRU) алгоритм. Параметры функции lru_cache

Алгоритм Least Recently Used (LRU) хранит maxsize самых последних используемых вызовов. Иными словами, вызовы, которые давно не используются выбрасываются из кэша, если он переполнен. Один из способов реализовать алгоритм LRU — это использовать двусвязный список из результатов (это параметры и вызовы функции) и отображений ключей на указатели списка. При добавлении ключа указатель на первый элемент списка меняется на указатель данного ключа. При этом элемент в списке с этим ключом удаляется, ведь он все равно станет первым. Хотя в Python алгоритм LRU устроен немного сложнее, например, он также учитывает свое использование в тредах [2].

Итак, до этого мы использовали lru_cache без явного задания параметров. Но на самом деле по умолчанию неявно передается maxsize=128. Давайте зададим явно этот параметр, равным 3:

@ftl.lru_cache(maxsize=3)
def mysum(n):
    if n == 1:
        return n
    print(f"'{n}'", end=" ")
    return n + mysum(n - 1)

>>> mysum(10)
'10' '9' '8' '7' '6' '5' '4' '3' '2' 55
>>> mysum(8)
36
>>> mysum(7)
'7' '6' '5' '4' '3' '2' 28

Наша функция, теперь хранит только 3 последних вызова, т.е. значения 10, 9, 8 (не забывайте, что этой линейной нехвостовой рекурсии нужно возвратиться обратно, когда n = 1). А вот значения 7 и меньше в кэше не хранятся, поэтому функция вычисляется как и положено. С другой стороны, если мы захотим теперь снова вычислить mysum(10), то вычислить нужно только mysum(8), mysum(9) и mysum(10):

>>> mysum(10)
'10' '9' '8' 55

Второй параметр декоратора lru_cache является typed, по умолчанию равный False. Если он равен True, то параметры декорируемой функции будут кэшированы отдельно. По умолчанию все параметры рассматриваются как эквивалентные. Это значит, что в некоторых случаях int может быть эквивалентен float (1 == 1.0) или список эквивалентен кортежу. В нашей рекурсивной функции даже передача True не гарантирует эквивалентности между int и float.

Также мы можем передать maxsize=None, это сделает кэш бесконечным. В Python 3.9 появилась функция cache, которая эквивалентна lru_cache(maxsize=None).

Изучаем информацию о кэше

Кэшированные функции имеют метод cache_info, который выводит информацию о кэше:

>>> mysum.cache_info()
CacheInfo(hits=2, misses=20, maxsize=3, currsize=3)

где

  • hits — количество сохраненных вызовов, что были извлечены из кэша. Действительно, вызвав функцию с параметром 8, мы достали из кэша значение mysum(8); с параметром 7, пришлось заново вычислять функцию; затем вызвав с параметром 10, мы достали из кэша значение mysum(7)
  • misses — количество раз когда, значение не было в кэше. Действительно, mysum(10) вычисляла все 10 значений, mysum(8) — просто достала значение из кэша, mysum(7) — вычисляла 7 значений, mysum(10) — вычисляла только значения 8, 9 и 10
  • maxsize — максимальное число значений, которые может хранить кэш
  • currsize — сколько значений занято в кэше на данный момент

Кэшируем геттеры класса с помощью cached_property

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

class A:
    def __init__(self, data):
        self.data = data

    @ftl.cached_property
    def sumdata(self):
        return sum(self.data)

>>> a = A([1, 2, 3, 4])
>>> a.sumdata
10
>>> a.sumdata # Заново не вычисляется
10

NLP с Python

Код курса
PNLP
Ближайшая дата курса
4 июля, 2022
Длительность обучения
40 ак.часов
Стоимость обучения
75 000 руб.

Еще больше подробностей о кэше в Python вы узнаете на наших образовательных курсах в лицензированном учебном центре обучения и повышения квалификации руководителей и IT-специалистов (менеджеров, архитекторов, инженеров, администраторов, Data Scientist’ов и аналитиков Big Data) в Москве:

Источники
  1. lru_cache functools
  2. lru_cache src
Комментировать