Слушайте нажатие клавиш с помощью asyncio
Может ли кто-нибудь предоставить пример кода, который прослушивает нажатие клавиши неблокирующим способом с помощью asynio и помещает код клавиши в консоль при каждом нажатии?
Это не вопрос какого-то графического инструментария
5 ответов
Таким образом, ссылка, предоставленная Андреа Корбеллини, является умным и тщательным решением проблемы, но также довольно сложной. Если все, что вы хотите сделать, это попросить пользователя ввести какой-либо ввод (или имитировать 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()
Я написал нечто подобное как часть пакета под названием aioconsole.
Это обеспечивает сопрограмму под названием get_standard_streams
который возвращает два потока asyncio, соответствующих stdin
а также stdout
,
Вот пример:
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: ')
Он должен работать как для файловых, так и для не файловых потоков. Смотрите реализацию здесь.
Линии чтения
Высокоуровневый чистый асинхронный способ сделать это выглядит следующим образом.
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()
возвращаться
''
сразу после
reader.at_eof()
вернул False. См. Подробности в Python asyncio: StreamReader .
Здесь
readline()
асинхронно собирает строку ввода. То есть цикл обработки событий может выполняться, пока читатель ожидает новых символов. Напротив, в других ответах цикл событий может блокироваться: он может обнаружить, что некоторый ввод доступен, введите вызов функции
sys.stdin.readline()
, а затем заблокируйте его, пока не станет доступна конечная строка (блокируя вход любых других задач в цикл). Конечно, в большинстве случаев это не проблема, поскольку конечная строка становится доступной вместе (в случае буферизации строк, которая используется по умолчанию) или очень скоро (в других случаях, предполагая достаточно короткие строки) с любыми начальными символами символа линия.
Чтение по буквам
Вы также можете читать отдельные байты с помощью
await reader.readexactly(1)
читать побайтно при чтении из канала. При чтении нажатий клавиш с терминала его необходимо правильно настроить, см. Прослушиватели ключей в 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, что приведет к двум итерациям цикла. Но этого достаточно для символов ASCII, таких как буквы и ESC.
Если вам нужны фактические нажатия клавиш, такие как ALT, я полагаю, единственный способ - использовать подходящую библиотеку и заставить ее работать с asyncio, вызвав ее в отдельном потоке, как здесь . На самом деле подход библиотека + поток, вероятно, проще и в других случаях.
Под капотом
Если вам нужен более точный контроль, вы можете реализовать свой собственный протокол вместо
StreamReaderProtocol
: класс, реализующий любое количество функций
<tcode id="4418396"></tcode>. Минимальный пример:
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
это пример
MyReadProtocol
(создается фабрикой протоколов, которая в нашем случае является тривиальной лямбдой), а
transport
это пример
asyncio.ReadTransport
(в частности, некоторая частная реализация, например
_UnixReadPipeTransport
в UNIX).
Но, в конце концов, все это шаблон, который в конечном итоге полагается на
<tcode id="4418409"></tcode> (не имеет отношения к
StreamReader
).
Для Windows вам может потребоваться выбрать
ProactorEventLoop
(значение по умолчанию, начиная с Python 3.8), см. Python asyncio: Поддержка платформы .
Альтернативой использованию очередей было бы сделать командную строку асинхронным генератором и обрабатывать команды по мере их поступления, вот так:
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()
Обновление Python 3.10 для решения, предоставленного user618895:
class Prompt:
def __init__(self):
self.loop = asyncio.get_running_loop()
self.q = asyncio.Queue()
self.loop.add_reader(sys.stdin, self.got_input)
def got_input(self):
asyncio.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)
# https://docs.python.org/3/library/asyncio-task.html#coroutine
task = asyncio.create_task(self.q.get())
return (await task).rstrip('\n')
Я протестировал его на клиенте веб-сокета внутри асинхронной функции, которая зависала в ожидании ввода, поэтому я заменил
s = input("insert string")
с
s = await prompt("insert string")
и теперь пинг-понг работает, даже когда программа ожидает ввода пользователя, соединение больше не останавливается, и проблема «время ожидания проверки активности понга истекло» решена.