functools.lru_cache не работает с асинхронными функциями
Хочу кэшировать результат асинхронной функции с помощью lru_cache:
import asyncio
from functools import lru_cache
@lru_cache
async def foo(x):
print("Вычисляю...")
await asyncio.sleep(1)
return x * 2
async def main():
print(await foo(10))
print(await foo(10))
asyncio.run(main())
Ожидал, что вторая строка сразу вернёт закэшированный результат, но вместо этого получаю ошибку:
- RuntimeError: cannot reuse already awaited coroutine
Не понимаю что делаю не так, кажется как-то не правильно использую lru_cache с async def.
Наверняка же есть стандартный\альтернативный способ кэшировать асинхронные функции?
Ответы (3 шт):
Вычисления отдельно (их можно кэшировать), ввод/вывод отдельно (его в принципе нельзя кэшировать):
import asyncio
from functools import lru_cache
@lru_cache
def foo_calc(x):
return x * 2
async def foo(x):
print("Вычисляю...")
await asyncio.sleep(1)
return foo_calc(x)
async def main():
print(await foo(10))
print(await foo(10))
asyncio.run(main())
Что бы понять как кешировать результат асинхронной функции, предлагаю сперва разобраться почему падает ошибка:
RuntimeError: cannot reuse already awaited coroutine
Для этого приведу пару примеров:
import asyncio
async def foo(x):
print("Вычисляю...")
await asyncio.sleep(1)
return x * 2
async def main():
coro1 = foo(10)
coro2 = foo(10)
print("Первый вызов вернул:", coro1)
print("Второй вызов вернул:", coro2)
print("Результат 1:", await coro1)
print("Результат 2:", await coro2)
asyncio.run(main())
Вывод:
Первый вызов вернул: <coroutine object foo at 0x00000000030B1780>
Второй вызов вернул: <coroutine object foo at 0x00000000030B1F00>
Вычисляю...
Результат 1: 20
Вычисляю...
Результат 2: 20
Process finished with exit code 0
Что мы видим?
- Каждый вызов
foo(10)создаёт новую корутину (разные адреса в памяти). awaitработает штатно, но и результат считается заново.
Пример второй, пытаемся кэшировать результат асинхронной функции с помощью lru_cache:
import asyncio
import inspect
from functools import lru_cache
@lru_cache
async def foo(x):
print("Вычисляю...")
await asyncio.sleep(0.1)
return x * 2
async def main():
coro1 = foo(10)
print("Первый вызов вернул:", coro1)
print("Состояние coro1:", inspect.getcoroutinestate(coro1))
result1 = await coro1
print("Результат 1:", result1)
print("Состояние coro1 после await:", inspect.getcoroutinestate(coro1))
# Второй вызов возвращает тот же объект!
coro2 = foo(10)
print("\nВторой вызов вернул:", coro2)
print("Состояние coro2:", inspect.getcoroutinestate(coro2))
# Тут будет ошибка, потому что объект уже CLOSED
result2 = await coro2
print("Результат 2:", result2)
asyncio.run(main())
Вывод:
Первый вызов вернул: <coroutine object foo at 0x00000000030A1780>
Состояние coro1: CORO_CREATED
Вычисляю...
Результат 1: 20
Состояние coro1 после await: CORO_CLOSED
Второй вызов вернул: <coroutine object foo at 0x00000000030A1780>
Состояние coro2: CORO_CLOSED
RuntimeError: cannot reuse already awaited coroutine
Process finished with exit code 1
Для большей наглядности я воспользовался модулем inspect — это стандартный модуль из коробки, так что его дополнительно не нужно устанавливать. Подробнее ознакомится с модулем можно в документации. А нас сейчас интересует вот эта часть:
inspect.getcoroutinestate(coroutine) Get current state of a coroutine object. The function is intended to be used with coroutine objects created by async def functions, but will accept any coroutine-like object that has cr_running and cr_frame attributes.
Possible states are:
CORO_CREATED: Waiting to start execution.
CORO_RUNNING: Currently being executed by the interpreter.
CORO_SUSPENDED: Currently suspended at an await expression.
CORO_CLOSED: Execution has completed.
И вот теперь всё понятно:
- При создании корутина в состоянии CORO_CREATED.
- После
awaitпереходит в состояние CORO_CLOSED. - При повторном вызове с
lru_cacheвозвращается тот же объект в состоянии CLOSED.
И вот поэтому await падает! Ведь повторная попытка вызвать await уже отработанного объекта как раз таки и вызывает RuntimeError.
Теперь немного о том, как работает lru_cache, что бы окончательно всё прояснить:
lru_cacheкэширует именно возвращаемое значение функции.- В нашем случае c
async def fooвозвращаемым значением является корутина, а не результат её выполнения.
То есть при вызове foo(10) мы получаем не число 20, а объект корутины <coroutine object foo at ...>:
- И именно этот объект попадает в кэш, а не итог вычислений.
print(type(foo(10))) # <class 'coroutine'>
Как решить эту проблему я здесь разбирать уже не буду — но отмечу, что товарищ CrazyElf любезно поделился ссылкой на похожий вопрос с enSO , где обсуждалось довольно много различных стратегий для решения данной задачи.