用asyncio收听按键

时间:2016-02-05 12:09:06

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

有人可以提供一个代码示例,它使用asynio以非阻塞方式监听按键,并在每次点击时将键码放入控制台吗?

这不是关于某些图形工具包的问题

4 个答案:

答案 0 :(得分:6)

因此,Andrea Corbellini提供的链接是解决问题的一个聪明而彻底的解决方案,但也非常复杂。如果您只想提示您的用户输入一些输入(或模拟raw_input),我更喜欢使用更简单的解决方案:

import sys
import functools
import asyncio as aio

class Prompt:
    def __init__(self, loop=None):
        self.loop = loop or aio.get_event_loop()
        self.q = aio.Queue(loop=self.loop)
        self.loop.add_reader(sys.stdin, self.got_input)

    def got_input(self):
        aio.ensure_future(self.q.put(sys.stdin.readline()), loop=self.loop)

    async def __call__(self, msg, end='\n', flush=False):
        print(msg, end=end, flush=flush)
        return (await self.q.get()).rstrip('\n')

prompt = Prompt()
raw_input = functools.partial(prompt, end='', flush=True)

async def main():
    # wait for user to press enter
    await prompt("press enter to continue")

    # simulate raw_input
    print(await raw_input('enter something:'))

loop = aio.get_event_loop()
loop.run_until_complete(main())
loop.close()

答案 1 :(得分:2)

我写了类似于名为aioconsole的软件包的一部分。

它提供了一个名为get_standard_streams的协程,它返回与stdinstdout对应的两个asyncio streams

以下是一个例子:

import asyncio
import aioconsole

async def echo():
    stdin, stdout = await aioconsole.get_standard_streams()
    async for line in stdin:
        stdout.write(line)

loop = asyncio.get_event_loop()
loop.run_until_complete(echo())

它还包含与input

等效的异步
something = await aioconsole.ainput('Entrer something: ') 

它应该适用于文件流和非文件流。请参阅实施here

答案 2 :(得分:2)

使用队列的另一种方法是使命令行成为异步生成器,并在它们进入时处理命令,如下所示:

import asyncio
import sys

class UserInterface(object):

def __init__(self, task, loop):
    self.task = task
    self.loop = loop

    def get_ui(self):
        return asyncio.ensure_future(self._ui_task())

    async def _ui_cmd(self):
        while True:
            cmd = sys.stdin.readline()
            cmd = cmd.strip()
            if cmd == 'exit':
                self.loop.stop()
                return
            yield cmd

    async def _ui_task(self):
        async for cmd in self._ui_cmd():
            if cmd == 'stop_t':
                self.task.stop()
            elif cmd == 'start_t':
                self.task.start()

答案 3 :(得分:2)

阅读行

执行此操作的高级纯异步方法如下。

import asyncio
import sys

async def main():
        # Create a StreamReader with the default buffer limit of 64 KiB.
        reader = asyncio.StreamReader()
        pipe = sys.stdin
        loop = asyncio.get_event_loop()
        await loop.connect_read_pipe(lambda: asyncio.StreamReaderProtocol(reader), pipe)

        async for line in reader:
                print(f'Got: {line.decode()!r}')

asyncio.run(main())

async for line in reader 循环可以更明确地编写,例如如果要在循环内打印提示或捕获异常:

 while True:
            print('Prompt: ', end='', flush=True)
            try:
                line = await reader.readline()
                if not line:
                    break
            except ValueError:
                print('Line length went over StreamReader buffer limit.')
            else:
                print(f'Got: {line.decode()!r}')

一个空的 line(不是 '\n' 而是一个实际的空字符串 '')表示文件结束。请注意,await reader.readline() 可能会在 '' 返回 False 后立即返回 reader.at_eof()。有关详细信息,请参阅 Python asyncio: StreamReader

这里的 readline() 是异步收集一行输入。也就是说,事件循环可以在读取器等待更多字符时运行。相比之下,在其他答案中,事件循环可能会阻塞:它可以检测到某些输入可用,进入调用 sys.stdin.readline() 的函数,然后阻塞它直到有一个结束线可用(阻止任何其他任务进入循环)。当然,在大多数情况下这不是问题,因为结束线与(在行缓冲的情况下,这是默认值)一起可用或很快(在其他情况下,假设行相当短)的任何初始字符线。

逐字读取

从管道读取时,您还可以使用 await reader.readexactly(1) 读取单个字节以逐字节读取。从终端读取按键时,需要正确设置,查看 Key Listeners in python? 了解更多信息。在 UNIX 上:

import asyncio
import contextlib
import sys
import termios

@contextlib.contextmanager
def raw_mode(file):
    old_attrs = termios.tcgetattr(file.fileno())
    new_attrs = old_attrs[:]
    new_attrs[3] = new_attrs[3] & ~(termios.ECHO | termios.ICANON)
    try:
        termios.tcsetattr(file.fileno(), termios.TCSADRAIN, new_attrs)
        yield
    finally:
        termios.tcsetattr(file.fileno(), termios.TCSADRAIN, old_attrs)

async def main():
    with raw_mode(sys.stdin):
        reader = asyncio.StreamReader()
        loop = asyncio.get_event_loop()
        await loop.connect_read_pipe(lambda: asyncio.StreamReaderProtocol(reader), sys.stdin)

        while not reader.at_eof():
            ch = await reader.read(1)
            # '' means EOF, chr(4) means EOT (sent by CTRL+D on UNIX terminals)
            if not ch or ord(ch) <= 4:
                break
            print(f'Got: {ch!r}')

asyncio.run(main())

请注意,这并不是一次一个字符或一个键:如果用户按下一个组合键,给出一个多字节字符,如 ALT+E,按下 ALT 时不会发生任何事情,两个字节将被发送按下 E 时的终端,这将导致循环的两次迭代。但对于字母和 ESC 等 ASCII 字符已经足够了。

如果您需要像 ALT 这样的实际按键,我想唯一的方法是使用 a suitable library 并通过在单独的线程中调用它来使其与 asyncio 一起使用,例如 here。事实上,在其他情况下,库+线程方法可能也更简单。

引擎盖下

如果您想要更好的控制,您可以实现自己的协议来代替 StreamReaderProtocol:一个实现任意数量的 asyncio.Protocol 函数的类。最小示例:

class MyReadProtocol(asyncio.Protocol):
    def __init__(self, reader: asyncio.StreamReader):
        self.reader = reader

    def connection_made(self, pipe_transport):
        self.reader.set_transport(pipe_transport)

    def data_received(self, data: bytes):
        self.reader.feed_data(data)

    def connection_lost(self, exc):
        if exc is None:
            self.reader.feed_eof()
        else:
            self.reader.set_exception(exc)

您可以用自己的缓冲机制替换 StreamReader。在您调用 connect_read_pipe(lambda: MyReadProtocol(reader), pipe) 之后,将恰好有一次对 connection_made 的调用,然后是对 data_received 的任意多次调用(数据取决于终端和 Python 缓冲选项),最后恰好是一次对 connection_lost 的调用connect_read_pipe(在文件结尾或错误时)。如果您需要它们,(transport, protocol) 会返回一个元组 protocol,其中 MyReadProtocoltransport 的一个实例(由协议工厂创建,在我们的例子中是一个微不足道的lambda),而 asyncio.ReadTransport_UnixReadPipeTransport 的实例(特别是一些私有实现,如 UNIX 上的 StreamReader)。

但最终这些都是最终依赖于 loop.add_reader(与 ProactorEventLoop 无关)的样板。

对于 Windows,您可能需要选择 LookerNodeSDK(自 Python 3.8 以来的默认设置),请参阅 Python asyncio: Platform Support