协程中的条件基于是否再次调用?

kas*_*bah 5 python python-3.x python-asyncio

我正在尝试将这个关键的“去抖动”逻辑从 Javascript 翻译为 Python。

function handle_key(key) {
    if (this.state == null) {
        this.state = ''
    }
    this.state += key
    clearTimeout(this.timeout)
    this.timeout = setTimeout(() => {
        console.log(this.state)
    }, 500)
}

handle_key('a')
handle_key('b')
Run Code Online (Sandbox Code Playgroud)

这个想法是随后的按键会延长超时时间。Javascript 版本打印:

ab 
Run Code Online (Sandbox Code Playgroud)

我不想翻译 JS 超时函数,我宁愿使用惯用的 Python 使用 asyncio。我在 Python (3.5) 中的尝试如下,但它不起作用,因为它global_state实际上没有按我的预期更新。

import asyncio

global_state = ''

@asyncio.coroutine
def handle_key(key):
    global global_state
    global_state += key
    local_state = global_state
    yield from asyncio.sleep(0.5)
    #if another call hasn't modified global_state we print it
    if local_state == global_state:
        print(global_state)

@asyncio.coroutine
def main():
    yield from handle_key('a')
    yield from handle_key('b')

ioloop = asyncio.get_event_loop()
ioloop.run_until_complete(main())
Run Code Online (Sandbox Code Playgroud)

它打印:

a
ab
Run Code Online (Sandbox Code Playgroud)

我已经研究过 asyncio EventQueueCondition我不清楚如何使用它们来实现此目的。如何使用 Python 的 asyncio 实现所需的行为?

编辑

有关我想如何使用的更多详细信息handle_keys。我有一个异步函数来检查按键。

@asyncio.coroutine
def check_keys():
    keys = driver.get_keys()
    for key in keys:
        yield from handle_key(key)
Run Code Online (Sandbox Code Playgroud)

这又与其他程序任务一起安排

@asyncio.coroutine
def main():
    while True:
        yield from check_keys()
        yield from do_other_stuff()

ioloop = asyncio.get_event_loop()
ioloop.run_until_complete(main())
Run Code Online (Sandbox Code Playgroud)

asyncio.create_taskQeek 对and的使用asyncio.gather是有道理的。但我该如何在这样的循环中使用它呢?或者是否有另一种方法来安排允许调用“重叠”的异步任务handle_keys

如果您有兴趣,可以在 GitHub 上查看实际代码

Qee*_*eek 1

怎么了

基本上yield from xy()与正常的函数调用非常相似。函数调用和函数调用的区别yield from在于,函数调用立即开始处理被调用的函数。该yield from语句将调用的协程插入事件循环内的队列中,并将控制权交给事件循环,并决定将处理队列中的哪个协程。

以下是您的代码的作用的解释:

  1. 它将添加main到事件循环的队列中。
  2. 事件循环开始处理队列中的协程。
  3. 队列仅包含main协程,因此它会启动它。
  4. 代码命中yield from handle_key('a').
  5. 它将添加handle_key('a')到事件循环的队列中。
  6. 事件循环现在包含mainandhandle_key('a')但 main 无法启动,因为它正在等待 的结果handle_key('a')
  7. 因此事件循环启动handle_key('a').
  8. 它会做一些事情,直到它到达yield from asyncio.sleep(0.5)
  9. 现在事件循环包含main(),handle_key('a')sleep(0.5)
    • 正在main()等待 的结果handle_key('a')
    • 正在handle_key('a')等待 的结果sleep(0.5)
    • 睡眠没有依赖性,因此可以启动。
  10. 0.5秒后asyncio.sleep(0.5)返回。None
  11. 事件循环获取None并将其返回到handle_key('a')协程中。
  12. 返回值被忽略,因为它没有分配给任何东西
  13. 打印handle_key('a')密钥(因为状态没有改变)
  14. 最后的协handle_key程返回 None (因为没有 return 语句)。
  15. 返回None到主程序。
  16. 返回值再次被忽略。
  17. 代码点击yield from handle_key('b')并开始处理新的键。
  18. 它运行与步骤 5 相同的步骤(但使用密钥b)。

如何修复它

coroutinrmain替换为:

@asyncio.coroutine
def main(loop=asyncio.get_event_loop()):
    a_task = loop.create_task(handle_key('a'))
    b_task = loop.create_task(handle_key('b'))
    yield from asyncio.gather(a_task, b_task)
Run Code Online (Sandbox Code Playgroud)

将其loop.create_task添加到事件循环的队列中,然后handle_key('a')将控制权交给事件循环。从此时开始的事件循环包含、、和。handle_key('b')yield from asyncio.gather(a_task, b_task)handle_key('a')handle_key('b')gather(...)main()

  • 等待main()结果来自gather()
  • 等待gather()所有作为参数给出的任务完成
  • 和没有依赖性,因此可以启动它们handle_key('a')handle_key('b')

事件循环现在包含 2 个可以启动的协程,但它会选择哪一个呢?嗯...谁知道这取决于实施。因此,为了更好地模拟按键,这个替换应该更好一点:

@asyncio.coroutine
def main(loop=asyncio.get_event_loop()):
    a_task = loop.create_task(handle_key('a'))
    yield from asyncio.sleep(0.1)
    b_task = loop.create_task(handle_key('b'))
    yield from asyncio.gather(a_task, b_task)
Run Code Online (Sandbox Code Playgroud)

Python 3.5 奖励

从文档中:

与 asyncio 一起使用的协程可以使用 async def 语句来实现。

async def 类型的协程是在 Python 3.5 中添加的,如果不需要支持较旧的 Python 版本,建议使用。

这意味着您可以替换:

@asyncio.coroutine
def main():
Run Code Online (Sandbox Code Playgroud)

与较新的声明

async def main():
Run Code Online (Sandbox Code Playgroud)

如果您开始使用新语法,那么您还必须替换yield fromawait.