如何在描述符的 __get__ 中等待异步调用?

问题描述 投票:0回答:1

我想写一个可延迟加载的属性。第一次

__get__()
时可以加载数据,但是
__get__()
无法设置为异步。有没有办法等待
obj.load()
完成然后返回getter的返回值。

class LazyLoadableProperty:
    def __get__(self, obj, owner_class):
        if obj is None:
            return self
        if not self.__loaded:
            # task = asyncio.create_task(obj.load())
            # while True:
            #     break when task is finished
            self.__loaded = True
        return self.__get(obj) # __get() is a costume getter set when init.
    ...


class A(LazyLoadable):
    _name: str
    def __init__():
        self._name = ""

    async def load(): # LazyLoadable is a abc and LazyLoadable.load is an async func.
        # load from http
        asyncio.sleep(1)
        self._name = "Jax"

    @LazyLoadableProperty
    def name(self):
        return self._name


async def main():
    a = A()
    print(a.name)

asyncio.run(main())
  1. 如果我直接调用: RuntimeWarning: coroutine 'A.load' was never waiting
obj.load() 
  1. 如果使用 run_until_complete: RuntimeError: 此事件循环已在运行
asyncio.get_event_loop().run_until_complete(obj.load())

其他一些方法可能会修改太多我不想使用的现有代码:

  1. __get__()
    返回一个 Awaitable 并等待
    await a.name
  2. LazyLoadable 是 abc。很难将所有
    <subclass>.load()
    更改为同步功能。
python asynchronous async-await python-asyncio descriptor
1个回答
0
投票

你在这里错过了一个重要的概念。当你的脚本遇到这一行时:

print(a.name)

该值为

a.name
无法立即获得。这就是你想要做的事情的全部意义。考虑一下:当 main 等待时,该值如何变得
变得
可用?在您的评论中,您说这是通过 HTTP 调用实现的。但要进行 HTTP 调用,您必须执行代码。在单线程异步程序中,此类代码唯一可以执行的位置是在 main 函数之外的
another
任务中。为了将控制权交给另一个任务,您需要执行一个等待表达式。在您提供的代码中根本没有等待语句,因此您的方法没有机会工作。

您的主要功能必须如下所示:

async def main():
    a = A()
    print(await a.name)

await 表达式暂停

main
,直到另一个任务计算
a.name
的值。这可能不是您想要的,但如果
a.name
的值尚不可用,那么确实没有其他逻辑可能性。

正如您从这个小代码片段中看到的,表达式

a.name
将是一个可等待的对象。它也可以是财产——没有任何规则反对这一点。下面的代码,当您运行它时,会延迟一秒钟,然后打印“Jax”。

import asyncio

class A:
    _name: str
    def __init__(self):
        self._name = ""

    async def load_name(self):
        # load from http
        if not self._name:
            await asyncio.sleep(1)
            self._name = "Jax"
        return self._name

    @property
    async def name(self):
        return await self.load_name()

async def main():
    a = A()
    print(await a.name)

asyncio.run(main())

此脚本执行完全相同的操作。

import asyncio

class A:
    _name: str
    def __init__(self):
        self._name = ""

    async def load_name(self):
        # load from http
        if not self._name:
            await asyncio.sleep(1)
            self._name = "Jax"
        return self._name

    @property
    def name(self):
        return self.load_name()


async def main():
    a = A()
    print(await a.name)

asyncio.run(main())

第二个版本包含更少的击键。但我个人更喜欢第一个版本,它更清楚地表明属性

name
是一个可等待的对象。

无需创建特殊的类或发明特殊类型的属性。

我还修复了您在几个地方遗漏的

self
参数,并在
await
前面添加了必要的
asyncio.sleep

© www.soinside.com 2019 - 2024. All rights reserved.