Значительно ускорьте многоразовые задачи, требующие больших вычислительных ресурсов.

Вычислительно интенсивные задачи сейчас повсюду.

В наши дни мы часто используем ресурсоемкие методы, такие как LLM и генеративный ИИ.

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

Здесь нам помогает кэш LRU. LRU расшифровывается как «наименее недавно использовавшийся». Это одна из многих стратегий кэширования. Давайте сначала разберемся, как это работает.



Как @lru_cache работает в Python?

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

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

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

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



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

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



Где мы можем использовать lru_cache в нашей повседневной работе?

Рассмотрим длительный запрос к базе данных. Мы все были там: ждали, постукивая пальцами, чтобы база данных, наконец, ответила необходимыми данными. Это часто происходит, когда нам нужно перезапустить Jupyter Notebooks.

Теперь представьте, что вы ожидаете один раз, а затем мгновенно получаете данные в следующий раз. Это магия lru_cache.

Это касается не только запросов к базе данных. А как насчет API? Особенно те, у которых модель ценообразования с оплатой по факту использования, например OpenAI API. Если подсказка выглядит одинаково с lru_cache, мы вызываем один раз, платим за это и повторно используем результат без повторной оплаты.

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



И давайте не будем забывать вычислительные задачи. Если ваша функция выполняет обработку чисел, собирая данные и выводя результаты после тяжелых вычислений, lru_cache может значительно снизить нагрузку на ваши системные ресурсы. Это часто происходит, когда ваше приложение использует тяжелую модель машинного обучения, такую ​​как BERT LLM, или имеет комплексную функцию поиска.

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

Тестирование lru_cache на рекурсивных функциях

Вот типичный пример генерации чисел Фибоначчи. Посмотрите, сколько времени потребуется для работы с lru_caching и без него.

import time

def fib(n):
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

start_time = time.time()
print(fib(40))
print("Execution Time: ", time.time() - start_time)

>> 102334155
>> Execution Time: 19.45328450202942

Теперь давайте импортируем lru_cache из модуля functools и аннотируем им функцию.

import time
from functools import lru_cache

@lru_cache(maxsize=None) # Invinite cache
def fib(n):
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

start_time = time.time()
print(fib(40))
print("Execution Time: ", time.time() - start_time)

>> 102334155
>> Execution Time: 8.893013000488281e-05

В коде достигается значительная экономия времени за счет добавления декоратора `lru_cache` для запоминания по сравнению с версией без запоминания.

В первом примере (без `lru_cache`) вычисление 40-го числа Фибоначчи заняло примерно 19,45 секунды. Во втором примере (с `lru_cache`) вычисление 40-го числа Фибоначчи заняло примерно 8,89e-05 секунд.

Это примерно 99,99% экономии времени.

Вот график времени исполнения для различных чисел Фибоначчи. Обратите внимание, что некэшированная версия занимает экспоненциально большее время выполнения, тогда как кешированная версия требует совсем немного времени для еще более значимых чисел.



@lru_cache — не серебряная пуля

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



Кэшируемая функция должна быть хэшируемой.

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

Например, следующая функция является неизменной. Следовательно, lru_cache отлично работает:

@lru_cache(maxsize=128)
def add_ten(number):
    return number + 10

print(add_ten(5))

>> 15

Но следующего нет. Вызов этого вызовет ошибку.

@lru_cache(maxsize=128)
def add_to_list(lst, item):
    lst.append(item)
    return lst

print(add_to_list([1, 2, 3], 4))  

>> TypeError: unhashable type: 'list'


lru_cache не подходит, если ваша функция зависит от внешних источников

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

Следующий скрипт Python имитирует внешний источник и проверяет его:

import functools
import time


def simulate_external_source():
    """Simulate an external source that changes over time."""
    return time.time()


@functools.lru_cache()
def dependent_function():
    """Function that depends on an external source."""
    return simulate_external_source()


# Test lru_cache with a function that depends on external sources
print("Testing lru_cache with a function that depends on external sources:")
print("Initial call:", dependent_function())
time.sleep(1)  # Simulate a delay
print("Cached call:", dependent_function())  # Should return the same result
time.sleep(1)  # Simulate a delay
print("Updated call:", dependent_function())  # Should still return the same result

print()


@functools.lru_cache(maxsize=1)
def expiring_function():
    """Function with cached entries that expire over time."""
    return simulate_external_source()


# Test lru_cache with entries that expire over time
print("Testing lru_cache with entries that expire over time:")
print("Initial call:", expiring_function())
time.sleep(1)  # Simulate a delay
print("Expired call:", expiring_function())  # Should trigger recalculation
time.sleep(1)  # Simulate a delay
print("Expired call:", expiring_function())  # Should trigger recalculation again

print()

___________________
Output
-------------------
Testing lru_cache with a function that depends on external sources:
Initial call: 1685507725.6362917
Cached call: 1685507725.6362917
Updated call: 1685507725.6362917

Testing lru_cache with entries that expire over time:
Initial call: 1685507727.639048
Expired call: 1685507727.639048
Expired call: 1685507727.639048

Для таких случаев вам нужна технология кэширования, поддерживающая TTL.



Заключение

lru_cache — это очень простой метод хранения выходных данных функций, чтобы вычисления не приходилось выполнять снова и снова. Хотя это способствует экономии дорогостоящих вычислительных ресурсов, это работает не во всех случаях.

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

Спасибо за прочтение, друг! Передайте мне привет в LinkedIn, Twitter и Medium.

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