首页 文章

根据是否再次调用协同程序中的条件?

提问于
浏览
4

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

我不喜欢使用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但它不是't clear to me how to use them for this. How would you implement the desired behavior using Python' s 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 调用"overlap"的异步任务?

Actual code on GitHub if you are interested .

2 回答

  • 1

    怎么了?

    基本上 yield from xy() 与普通函数调用非常相似 . 函数调用和 yield from 之间的区别在于函数调用立即开始处理调用函数 . yield from 语句在事件循环内调用coroutine进入队列,并控制事件循环,并决定处理队列中的哪个协同程序 .

    以下是您的代码所做的解释:

    • 它将 main 添加到事件循环的队列中 .

    • 事件循环开始处理队列中的协同程序 .

    • 队列只包含 main 协同程序,因此启动它 .

    • 代码命中 yield from handle_key('a') .

    • 它在事件循环的队列中添加 handle_key('a') .

    • 事件循环现在包含 mainhandle_key('a') 但主要无法启动,因为它正在等待 handle_key('a') 的结果 .

    • 因此事件循环启动 handle_key('a') .

    • 它会做一些东西,直到它击中 yield from asyncio.sleep(0.5) .

    • 现在事件循环包含 main()handle_key('a')sleep(0.5) .

    • main() 正在等待 handle_key('a') 的结果 .

    • handle_key('a') 正在等待 sleep(0.5) 的结果 .

    • 睡眠没有依赖性,因此可以启动它 .

    • asyncio.sleep(0.5) 在0.5秒后返回 None .

    • 事件循环采用 None 并将其返回到 handle_key('a') 协程 .

    • 忽略返回值,因为它未分配给任何内容

    • handle_key('a') 打印密钥(因为没有任何改变状态)

    • 最后的 handle_key 协程返回None(因为没有return语句) .

    • None 返回主页面 .

    • 再次忽略返回值 .

    • 代码命中 yield from handle_key('b') 并开始处理新密钥 .

    • 它从步骤5开始执行相同的步骤(但使用键 b ) .

    如何修复它

    main coroutinr替换为:

    @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奖金

    从文档:

    可以使用async def语句实现与asyncio一起使用的协同程序 . 在Python 3.5中添加了异步def类型的协程,如果不需要支持旧的Python版本,建议使用它 .

    这意味着你可以替换:

    @asyncio.coroutine
    def main():
    

    更新的声明

    async def main():
    

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

  • 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来开始执行一些协程"in background" . 如果您不希望它完成,您也可以cancel task(就像使用 clearTimeout(this.timeout) 一样) .

    模拟您的JavaScript代码段的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('s similar to Python' s async / await )中阅读 handle_key / await 并查看将其与回调/承诺进行比较的示例 . This article看起来不错 .

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

    Upd:

    • 由于 buttons.check 需要定期调用 driver.get_buttons() ,你必须使用循环 . 但它可以作为任务和事件循环一起完成 .

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

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

    • 这里有一些伪代码,显示处理按钮的后台任务,并使用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())
    

相关问题