【问题标题】:Conditional in a coroutine based on whether it was called again?基于是否再次调用协程的条件?
【发布时间】:2017-08-09 23:17:01
【问题描述】:

我正在尝试将这个关键的“去抖动”逻辑从 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')

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

ab 

我不想翻译 JS 超时函数,我宁愿使用 asyncio 的惯用 Python。我在 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())

打印出来:

a
ab

我已经研究了 asyncio Event, Queue and Condition,但我不清楚如何使用它们。您将如何使用 Python 的 asyncio 实现所需的行为?

编辑

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

@asyncio.coroutine
def check_keys():
    keys = driver.get_keys()
    for key in keys:
        yield from handle_key(key)

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

@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())

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

Actual code on GitHub if you are interested.

【问题讨论】:

    标签: python python-3.x python-asyncio


    【解决方案1】:

    为什么你的代码现在不起作用?

    handle_key 两个 JavaScript 函数都不会阻止执行。每个只是清除超时回调并设置新的。它立即发生。

    协程以另一种方式工作:在协程上使用 yield fromnewer syntax await 意味着我们只想在协程完全完成后恢复执行流程:

    async def a():
        await asyncio.sleep(1)
    
    async def main():
        await a()
        await b()  # this line would be reached only after a() done - after 1 second delay
    

    您的代码中的asyncio.sleep(0.5) - 不是通过超时设置回调,而是应该在handle_key finsihed 之前完成的代码。

    让我们试着让代码工作

    您可以创建task 以“在后台”开始执行一些协程。如果您不想完成,也可以cancel task(就像您使用clearTimeout(this.timeout) 一样)。

    模拟您的 javascript sn-p 的 Python 版本:

    import asyncio
    from contextlib import suppress
    
    global_state = ''
    timeout = None
    
    async def handle_key(key):
        global global_state, timeout
    
        global_state += key
    
        # cancel previous callback (clearTimeout(this.timeout))
        if timeout:
            timeout.cancel()
            with suppress(asyncio.CancelledError):
                await timeout
    
        # set new callback (this.timeout = setTimeout ...)
        async def callback():
            await asyncio.sleep(0.5)
            print(global_state)
        timeout = asyncio.ensure_future(callback())
    
    
    async def main():
        await handle_key('a')
        await handle_key('b')
    
        # both handle_key functions done, but task isn't finished yet
        # you need to await for task before exit main() coroutine and close loop
        if timeout:
            await timeout
    
    loop = asyncio.get_event_loop()
    try:
        loop.run_until_complete(main())
    finally:
        loop.close()
    

    惯用语?

    虽然上面的代码有效,但 asyncio 不应该这样使用。您的 javascript 代码基于回调,而 asyncio 通常将避免使用回调。

    很难在您的示例中展示差异,因为它本质上是基于回调的(键处理 - 是某种全局回调)并且没有更多的异步逻辑。但是,当您稍后添加更多异步操作时,这种理解会很重要。

    现在我建议您阅读现代 javascript 中的 async/await(类似于 Python 的 async/await),并查看将其与回调/承诺进行比较的示例。 This article 看起来不错。

    它将帮助您了解如何在 Python 中使用基于协程的方法。

    更新:

    1. 由于buttons.check 需要定期调用driver.get_buttons(),因此您必须使用循环。但它可以与您的事件循环一起作为任务完成。

      如果您有某种button_handler(callback)(这通常是不同的库允许处理用户输入的方式),您可以使用它直接设置一些asyncio.Future 并避免循环。

    2. 考虑可能从一开始就用asyncio 编写一些小gui 应用程序。我认为它可以帮助您更好地了解如何调整现有项目。

    3. 这里有一些伪代码显示要处理的后台任务 按钮并使用 asyncio 处理一些简单的 UI 事件/状态逻辑:

    .

    import asyncio
    from contextlib import suppress
    
    
    # GUI logic:
    async def main():
        while True:
            print('We at main window, popup closed')
    
            key = await key_pressed
            if key == 'Enter':
                print('Enter - open some popup')
    
                await popup()
                # this place wouldn't be reached until popup is not closed
    
                print('Popup was closed')
    
            elif key == 'Esc':
                print('Esc - exit program')
                return
    
    
    async def popup():
        while True:
            key = await key_pressed
            if key == 'Esc':
                print('Esc inside popup, let us close it')
                return
            else:
                print('Non escape key inside popup, play sound')
    
    
    # Event loop logic:
    async def button_check():
        # Where 'key_pressed' is some global asyncio.Future
        # that can be used by your coroutines to know some key is pressed
        while True:
            global key_pressed
            for key in get_buttons():
                key_pressed.set_result(key)
                key_pressed = asyncio.Future()
            await asyncio.sleep(0.01)
    
    
    def run_my_loop(coro):
        loop = asyncio.get_event_loop()
    
        # Run background task to process input
        buttons_task = asyncio.ensure_future(button_check())
    
        try:
            loop.run_until_complete(main())
        finally:
    
            # Shutdown task
            buttons_task.cancel()
            with suppress(asyncio.CancelledError):
                loop.run_until_complete(buttons_task)
    
            loop.close()
    
    
    if __name__ == '__main__':
        run_my_loop(main())
    

    【讨论】:

    • 感谢您的回答,我对翻译实际的回调/超时逻辑不太感兴趣,但考虑如何在 JS 中使用 async/await 是有道理的。我在我的问题中添加了一些关于我想如何使用handle_keys 的更多细节。
    • @kasbah 我更新了答案,希望对您有所帮助。正如我所说,从一开始就考虑使用 asyncio 编写一些小 GUI 项目的可能性。解决 asyncio 相关问题会更容易,并且会更容易理解如何使 asyncio 适应现有项目。
    • 所以如果driver.get_buttons() 是异步的,我可以避免在“同步”循环中调用所有内容,而handle_keys 调用可能会重叠?
    • @kasbah 完全正确。通常 GUI 允许通过回调来获取新闻(或其他)事件,例如 on_key_press(callback)。您可以在此事件上注册刚刚设置某些asyncio.Future 的结果(按键)的回调,并在您的asyncio 代码中等待这个未来。换句话说,asyncio.Future/asyncio.Event - 是从回调世界到asyncio 世界的适配器。如果您没有回调,则需要创建一些循环来将同步代码转换为异步代码。
    【解决方案2】:

    怎么了

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

    以下是您的代码功能的解释:

    1. 它将main 添加到事件循环的队列中。
    2. 事件循环开始处理队列中的协程。
    3. 队列仅包含 main 协程,因此它会启动它。
    4. 代码命中yield from handle_key('a')
    5. 它将handle_key('a') 添加到事件循环的队列中。
    6. 事件循环现在包含 mainhandle_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. asyncio.sleep(0.5) 在 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)。

    如何解决

    main cooutinr 替换为:

    @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)
    

    loop.create_taskhandle_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)
    

    Python 3.5 奖励

    来自文档:

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

    在 Python 3.5 中加入了 async def 类型的协程,如果不需要支持旧的 Python 版本,推荐使用。

    表示可以替换:

    @asyncio.coroutine
    def main():
    

    使用较新的语句

    async def main():
    

    如果您开始使用新语法,则还必须将 yield from 替换为 await

    【讨论】:

    • 很有道理,但是如果我在循环中调用 handle_keys 和其他函数,我什么时候会收集任务?我在编辑我的问题时提供了更多详细信息。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-12-16
    • 2013-11-12
    • 2020-09-04
    • 2014-01-29
    • 1970-01-01
    • 2010-12-04
    相关资源
    最近更新 更多