Conditional in a coroutine based on whether it was called again?
I am trying to translate this key "debouncing" logic from Javascript to 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')
The idea is that subsequent key presses extend the timeout. The Javascript version prints:
ab
I don't want to translate the JS timeout functions, I'd rather have idiomatic Python using asyncio. My attempt in Python (3.5) is below, but it doesn't work as global_state
is not actually updated when I expect.
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())
It prints:
a
ab
I have looked into asyncio Event
, Queue
and Condition
but it isn't clear to me how to use them for this. How would you implement the desired behavior using Python's asyncio?
EDIT
Some more details on how I'd like to use handle_keys
. I have an async function that checks for key presses.
@asyncio.coroutine
def check_keys():
keys = driver.get_keys()
for key in keys:
yield from handle_key(key)
Which in turn is scheduled along with other program tasks
@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
makes sense. But how would I use it within a loop like this? Or is there another way to schedule the async tasks that would allow handle_keys
calls to "overlap"?
Actual code on GitHub if you are interested.
What's wrong
Basically the yield from xy()
is very similar to the normal function call. The difference between function call and yield from
is that the function call immediately start processing called function. The yield from
statement insert called coroutine into queue inside event loop and give control to event loop and it decide which coroutine in it's queue will be processed.
Here is the explanation of what you code does:
main
into event loop's queue. main
coroutine so it starts that. yield from handle_key('a')
. handle_key('a')
in the event loop's queue. main
and handle_key('a')
but the main cannot be started because it is waiting for the result of the handle_key('a')
. handle_key('a')
. yield from asyncio.sleep(0.5)
. main()
, handle_key('a')
and sleep(0.5)
. main()
is waiting for result from handle_key('a')
. handle_key('a')
is waiting for result from sleep(0.5)
. asyncio.sleep(0.5)
returns None
after 0.5 second. None
and return it into the handle_key('a')
coroutine. handle_key('a')
prints the key (because nothing change the state) handle_key
coroutine at the end return None (because there isn't return statement). None
is returned to the main. yield from handle_key('b')
and start processing new key. b
). How to fix it
The main
coroutinr replace with this:
@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)
The loop.create_task
adds into the event loop's queue the handle_key('a')
and handle_key('b')
and then the yield from asyncio.gather(a_task, b_task)
give control to the event loop. The event loop from this point contains handle_key('a')
, handle_key('b')
, gather(...)
and main()
.
main()
wiating for result from gather()
gather()
waiting until all tasks given as parameters are finished handle_key('a')
and handle_key('b')
has no dependencies so they can be started. The event loop now contains 2 coroutine which can start but which one will it pick? Well... who knows it is implementation depended. So for better simulation of pressed keys this one replace should be a little better:
@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 bonus
From the documentation:
Coroutines used with asyncio may be implemented using the async def statement.
The async def type of coroutine was added in Python 3.5, and is recommended if there is no need to support older Python versions.
It means that you can replace:
@asyncio.coroutine
def main():
with newer statement
async def main():
If you start using the new syntax then you have to also replace yield from
with await
.
Why your code doesn't work now?
Both handle_key
javascript functions don't block execution. Each just clear timeout callback and set new one. It happens immediately.
Coroutines work another way: using yield from
or newer syntax await
on coroutine means that we want to resume execution flow only after this coroutine if fully done:
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)
in your code - is not setting callback by timeout, but code that should be done before handle_key
finsihed.
Let's try to make code work
You can create task to start execution some coroutine "in background". You can also cancel task (just like you do with clearTimeout(this.timeout)
) if you don't want it to be finished.
Python version that emulates your javascript snippet:
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()
Idiomatic?
While code above works, it is not how asyncio
should be used. Your javascript code based on callbacks, while asyncio
usually is about to avoid using of callbacks.
It's hard to demonstrate difference on your example since it's callback based by nature (key handling - is some sort of global callback) and doesn't have more async logic. But this understanding would be important later when you'll add more async operations.
Right now I advice you to read about async
/ await
in modern javascript (it's similar to Python's async
/ await
) and look at examples comparing it to callbacks/promises. This article looks good.
It'll help you understand how you can use coroutine-based approach in Python.
Upd:
Since buttons.check
needs to periodically call driver.get_buttons()
you'll have to use loop. But it can be done as task along with your event loop.
If you had some sort of button_handler(callback)
(this is usually how different libs allow to handle user input) you could use it to set some asyncio.Future
directly and avoid loop.
Consider possibility write some little gui app with asyncio
from the beginning. I think it may help you to better understand how you can adapt your existing project.
Here's some pseudo-code that shows background task to handle buttons and using asyncio to handle some simple UI events/states logic:
.
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())
链接地址: http://www.djcxy.com/p/53230.html
上一篇: 调用协程并在asyncio.Protocol.data中获得未来
下一篇: 基于它是否被再次调用,在协程中有条件?