在普通函数中调用异步函数

13

我对Python 3.6的asyncio相当新手

我的问题是,我有一个类,想在其中初始化一些属性。其中一个属性是来自异步函数的返回值。

最佳实践是什么?

  1. 在init函数中调用event_loop一次以获取返回值?

  2. 将__init__函数设置为async? 并在事件循环中运行?

干杯!

以下是我的代码:

import asyncio
import aioredis
from datetime import datetime

class C:
    def __init__(self):
        self.a = 1
        self.b = 2
        self.r = None
        asyncio.get_event_loop().run_until_complete(self._async_init())

    async def _async_init(self):
        # this is the property I want, which returns from an async function
        self.r = await aioredis.create_redis('redis://localhost:6379')

    async def heart_beat(self):
        while True:
            await self.r.publish('test_channel', datetime.now().__str__())
            await asyncio.sleep(10)

    def run(self):
        asyncio.get_event_loop().run_until_complete(self.heart_beat())

c=C()
c.run()

这有点取决于函数为什么是异步的,类将如何使用等。我知道编写一个包含足够信息以传达非平凡异步程序相关思想的[mcve]很难编写,但如果没有[mcve]更难回答。 - abarnert
@abarnert 我刚刚编辑了这个问题。 - qichao_he
__init__异步化的最佳实践是不要这样做。如果您尝试在事件循环已经运行时实例化一个实例,它将无法工作。 - dirn
1
没错。我并没有说它永远不会起作用。我举了一个它不起作用的例子,这也是@user4815162342在他们的回答中提到的例子。 - dirn
@dirn 啊,我明白了。现在我懂了,谢谢! - qichao_he
显示剩余2条评论
1个回答

10
在init函数中调用一次event_loop以获取返回值吗?
如果在__init__期间旋转事件循环,则无法在事件循环运行时实例化C; asyncio事件循环不嵌套
[编辑:在对问题进行第二次更新后,似乎事件循环由非静态方法C.run运行,因此run_until_complete在编写的代码中可以在__init__中工作。 但是这种设计是有限制的-例如,在事件循环正在运行时,它不允许构造另一个实例C或类似C的类。]
__init__函数设置为async并在事件循环中运行? __init__不能被设置为async而不采用非常丑陋的黑客手段。 Python的__init__通过副作用运行,并必须返回None,而async def函数返回协程对象。
要使其正常工作,您有几个选项:

Async C工厂

创建一个异步函数,该函数返回C实例,例如:
async def make_c():
    c = C()
    await c._async_init()
    return c

这样的函数可以轻松地实现异步操作,并在需要时进行等待。如果您更喜欢静态方法而不是函数,或者如果您感到不舒服从未在类中定义的函数访问私有方法,则可以用C.create()替换 make_c()

异步 C.r 字段

通过在其中存储Future,您可以使r属性变为异步:

class C:
    def __init__(self):
        self.a = 1
        self.b = 2
        loop = asyncio.get_event_loop()
        # note: no `await` here: `r` holds an asyncio Task which will
        # be awaited (and its value accessed when ready) as `await c.r`
        self.r = loop.create_task(aioredis.create_redis('redis://localhost:6379'))

这将需要将c.r的每个使用拼写为await c.r。无论它是否可接受(甚至有益),都取决于程序中其他地方使用的位置和频率。
异步C构造函数
尽管__init__无法变成异步,但这一限制不适用于其低级别的同类__new__T.__new__可以返回任何对象,包括不是T的实例的对象,我们可以利用这一事实使其返回一个协程对象:
class C:
    async def __new__(cls):
        self = super().__new__(cls)
        self.a = 1
        self.b = 2
        self.r = await aioredis.create_redis('redis://localhost:6379')
        return self

# usage from another coroutine
async def main():
    c = await C()

# usage from outside the event loop
c = loop.run_until_complete(C())

我不建议在生产代码中使用这种方法,除非你有一个非常好的理由。

  • 这是对构造函数机制的滥用,因为它定义了一个不返回C实例的C.__new__构造函数;
  • Python会注意到上述问题,即使你定义或继承了它(同步)的实现,也会拒绝调用C.__init__
  • 使用await C()看起来非常不符合惯用法,即使(或尤其是)对于习惯于asyncio的人来说也是如此。

真的不允许从“None”返回值吗?我可能从未尝试过,因为它有用的情况远远少于它是无意义和误导的情况,但如果它是非法的,我会有点惊讶。(我不会对linter标记它感到惊讶,静态分析器标记类的每个构造,因为在构造机器内部忽略了返回值等等,只是Python解释器拒绝它或参考文档明确禁止它。) - abarnert
@abarnert 在 __init__ 中返回任何非 None 值都会引发 TypeError,这是由默认元类实现的 <type>.__call__ 强制执行的。可以使用自定义元类覆盖此行为,但这将是一种不同的初始化协议,其中 __init__ 不再通过副作用操作。 - user4815162342
一个更有前途的路线可能是使用 async def __new__(...),但很难确定这是否会在某个地方导致破坏,例如 Python 在测试 T.__new__ 是否返回 T 实例并使用它来决定是否调用 T.__init__ 以及其他类似的地方。覆盖 T.__new__MetaT.__call__ 以返回协程/未来对象是答案中所谓的“丑陋的黑科技”。这可能是可行的,但绝不是推荐的首选方案。 - user4815162342
现在我很好奇他们是什么时候添加了这个检查。它是从2.3还是2.2开始的?无论如何,是的,从__new__返回一个协程应该是可行的。我曾经在Greg Ewing的预异步化yield from调度器示例中执行过等效操作,当调试代码时非常棘手,但你可以让它工作。但在yield from之前仍然需要手动同步调用__init__,因此可能还需要另一步来实际解决此问题(即某人可能需要更多obj = await Cls(args)才能使用它)。我需要思考一下。 - abarnert

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接