在不导入 asyncio 包的情况下启动异步功能

Start async function without importing the asyncio package

是否可以启动这样的功能

async def foo():
    while True:
        print("Hello!")

不导入 asyncio 包(并获取事件循环)?

我正在寻找一种类似于 Go 的 goroutines 的原理,其中只需要 go 语句就可以启动协程。

编辑:我不导入 asyncio 包的原因仅仅是因为我认为应该可以在没有事件循环的情况下启动协程(显式)。我不明白为什么 async def 和类似的语句是核心语言的一部分(甚至是语法的一部分)并且启动创建的协程的方式只能通过 package.

不,那不可能。你需要一个事件循环。看看如果你只调用 foo():

会发生什么
>>> f = foo()
>>> print(f)
<coroutine object foo at 0x7f6e13edac50>

所以你得到了一个协程对象,现在什么都没有执行!只有将它传递给事件循环,它才会被执行。您可以使用 asyncio 或其他事件循环,例如 Curio.

Python 协程是生成器的语法糖,在它们的行为中增加了一些限制(因此它们的目的明显不同并且不会混合)。你不能这样做:

next(foo())
TypeError: 'coroutine' object is not an iterator

因为它被明确禁用。但是你可以这样做:

foo().send(None)
Hello
Hello
Hello
...

相当于 next() 生成器。

当然可以在不显式使用 asyncio 的情况下启动 async 函数。毕竟,asyncio 是用 Python 编写的,所以它所做的一切,你也可以做(尽管有时你可能需要其他模块,如 selectorsthreading,如果你打算同时等待外部事件,或并行执行一些其他代码)。

在这种情况下,由于您的函数内部没有 await 点,因此只需按一下即可开始。您通过 sending None 将协程推入其中。

>>> foo().send(None)
Hello!
Hello!
...

当然,如果你的函数(协程)内部有 yield 表达式,它会在每个 yield 点暂停执行,你需要将额外的值推入其中(通过 coro.send(value)next(gen)) - 但如果你知道发电机的工作原理,你就已经知道了。

import types

@types.coroutine
def bar():
    to_print = yield 'What should I print?'
    print('Result is', to_print)
    to_return = yield 'And what should I return?'
    return to_return

>>> b = bar()
>>> next(b)
'What should I print?'
>>> b.send('Whatever you want')
Result is Whatever you want
'And what should I return?'
>>> b.send(85)
Traceback...
StopIteration: 85

现在,如果您的函数内部有 await 个表达式,它将在计算每个表达式时暂停。

async def baz():
    first_bar, second_bar = bar(), bar()
    print('Sum of two bars is', await first_bar + await second_bar)
    return 'nothing important'

>>> t = baz()
>>> t.send(None)
'What should I print?'
>>> t.send('something')
Result is something
'And what should I return?'
>>> t.send(35)
'What should I print?'
>>> t.send('something else')
Result is something else
'And what should I return?'
>>> t.send(21)
Sum of two bars is 56
Traceback...
StopIteration: nothing important

现在,所有这些 .send 都开始变得乏味了。如果能半自动生成它们就好了。

import random, string

def run_until_complete(t):
    prompt = t.send(None)
    try:
        while True:
            if prompt == 'What should I print?':
                prompt = t.send(random.choice(string.ascii_uppercase))
            elif prompt == 'And what should I return?':
                prompt = t.send(random.randint(10, 50))
            else:
                raise ValueError(prompt)
    except StopIteration as exc:
        print(t.__name__, 'returned', exc.value)
        t.close()

>>> run_until_complete(baz())
Result is B
Result is M
Sum of two bars is 56
baz returned nothing important

恭喜,您刚刚编写了第一个事件循环! (没想到它会发生,是吗?;)当然,它非常原始:它只知道如何处理两种类型的提示,它不会启用 t 来生成额外的协程 运行 与它并发,它通过 random 生成器伪造事件。

(事实上,如果你想获得哲理:我们在上面手动完成的操作,可以称为事件循环吗:Python REPL 正在打印提示到控制台 window,它依赖于您通过在其中键入 t.send(whatever) 来提供事件。:)

asyncio 只是上面的一个非常普遍的变体:提示被 Futures 取代,多个协程被保留在队列中,所以每个协程最终轮到它,并且事件很多更丰富,包括 network/socket 通信、文件系统 reads/writes、信号处理、thread/process 边执行等。但基本思想仍然是相同的:你抓取一些协程,在空中将它们从一个 Futures 路由到另一个,直到它们都提高 StopIteration。当所有协程都无事可做时,你就去外部世界抓一些额外的事件让它们咀嚼,然后继续。

我希望现在一切都清楚多了。 :-)

协程应该能够

  1. 运行

  2. 将控制权交给调用者(可选地产生一些中间 结果)

  3. 能够从来电者那里得到一些信息并恢复

所以,这是一个异步函数(又名本机协程)的小演示,它不使用 asyncio 或任何其他提供事件循环的 modules/frameworks 来完成。至少需要 python 3.5。请参阅代码中的注释。

#!/usr/bin/env python

import types

# two simple async functions
async def outer_af(x):
    print("- start outer_af({})".format(x))
    val = await inner_af(x)  # Normal way to call native coroutine.
                             # Without `await` keyword it wouldn't
                             # actually start
    print("- inner_af result: {}".format(val))
    return "outer_af_result"


async def inner_af(x):
    print("-- start inner_af({})".format(x))
    val = await receiver()  # 'await' can be used not only with native
                            # coroutines, but also with `generator-based`
                            # coroutines!
    print("-- received val {}".format(val))
    return "inner_af_result"


# To yiled execution control to caller it's necessary to use
# 'generator-based' coroutine: the one created with types.coroutine
# decorator
@types.coroutine
def receiver():
    print("--- start receiver")
    # suspend execution / yield control / communicate with caller
    r = yield "value request"
    print("--- receiver received {}".format(r))
    return r

def main():
    # We want to call 'outer_af' async function (aka native coroutine)
    # 'await' keyword can't be used here!
    # It can only be used inside another async function.
    print("*** test started")
    c = outer_af(42)  # just prepare coroutine object. It's not running yet.
    print("*** c is {}".format(c))

    # To start coroutine execution call 'send' method.
    w = c.send(None)  # The first call must have argument None

    # Execution of coroutine is now suspended. Execution point is on
    # the 'yield' statement inside the 'receiver' coroutine.
    # It is waiting for another 'send' method to continue.
    # The yielded value can give us a hint about what exectly coroutine
    # expects to receive from us.
    print("*** w = {}".format(w))

    # After next 'send' the coroutines execution would finish.
    # Even though the native coroutine object is not iterable it will
    # throw StopIteration exception on exit!
    try:
        w = c.send(25)
        # w here would not get any value. This is unreachable.
    except StopIteration as e:
        print("*** outer_af finished. It returned: {}".format(e.value))


if __name__ == '__main__':
    main()

输出如下:

*** test started
*** c is <coroutine object outer_af at 0x7f4879188620>
- start outer_af(42)
-- start inner_af(42)
--- start receiver
*** w = value request
--- receiver received 25
-- received val 25
- inner_af result: inner_af_result
*** outer_af finished. It returned: outer_af_result

补充评论。 看起来不可能从本机协同程序内部产生控制。 yield 不允许在 async 函数中使用!所以有必要 import types 并使用 coroutine 装饰器。它有一些黑魔法!坦率地说,我不明白为什么 yield 被禁止,因此需要混合使用原生和基于生成器的协程。