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

时间:2017-08-09 03:34:14

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

我正试图翻译这个键" debouncing"从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

2 个答案:

答案 0 :(得分:1)

出了什么问题

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

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

  1. 它将main添加到事件循环的队列中。
  2. 事件循环开始处理队列中的协程。
  3. 队列只包含main协程,因此它会启动。
  4. 代码点击yield from handle_key('a')
  5. 它在事件循环队列中添加了handle_key('a')
  6. 事件循环现在包含mainhandle_key('a')但主要无法启动,因为它正在等待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(因为没有返回语句)。
  15. None将返回主页。
  16. 再次忽略返回值。
  17. 代码点击yield from handle_key('b')并开始处理新密钥。
  18. 它从步骤5开始执行相同的步骤(但使用键b)。
  19. 如何解决

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

    来自文档:

      

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

         

    在Python 3.5中添加了异步def类型的协同程序,如果不需要支持旧的Python版本,建议使用它。

    这意味着您可以替换:

    @asyncio.coroutine
    def main():
    

    更新的声明

    async def main():
    

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

答案 1 :(得分: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片段的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中使用基于协程的方法。

<强> UPD:

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

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

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

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

  4. 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())