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 шт):

Автор решения: dim5x

Попробуйте async-lru.

pip install async-lru

https://pypi.org/project/async-lru/

→ Ссылка
Автор решения: Stanislav Volodarskiy

Вычисления отдельно (их можно кэшировать), ввод/вывод отдельно (его в принципе нельзя кэшировать):

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())
→ Ссылка
Автор решения: Amgarak

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

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 , где обсуждалось довольно много различных стратегий для решения данной задачи.

→ Ссылка