简体   繁体   English

非阻塞控制台输入?

[英]Non-blocking console input?

I am trying to make a simple IRC client in Python (as kind of a project while I learn the language).我正在尝试在 Python 中创建一个简单的 IRC 客户端(作为我学习语言的项目)。

I have a loop that I use to receive and parse what the IRC server sends me, but if I use raw_input to input stuff, it stops the loop dead in its tracks until I input something (obviously).我有一个循环,用于接收和解析 IRC 服务器发送给我的内容,但如果我使用raw_input输入内容,它会停止循环在其轨道中死去,直到我输入一些内容(显然)。

How can I input something without the loop stopping?如何在不停止循环的情况下输入内容?

(I don't think I need to post the code, I just want to input something without the while 1: loop stopping.) (我认为我不需要发布代码,我只想在没有while 1:循环停止的情况下输入一些内容。)

I'm on Windows.我在 Windows。

For Windows, console only, use the msvcrt module:对于 Windows,仅控制台,使用msvcrt模块:

import msvcrt

num = 0
done = False
while not done:
    print(num)
    num += 1

    if msvcrt.kbhit():
        print "you pressed",msvcrt.getch(),"so now i will quit"
        done = True

For Linux, this article describes the following solution, it requires the termios module:对于 Linux, 本文描述了以下解决方案,它需要termios模块:

import sys
import select
import tty
import termios

def isData():
    return select.select([sys.stdin], [], [], 0) == ([sys.stdin], [], [])

old_settings = termios.tcgetattr(sys.stdin)
try:
    tty.setcbreak(sys.stdin.fileno())

    i = 0
    while 1:
        print(i)
        i += 1

        if isData():
            c = sys.stdin.read(1)
            if c == '\x1b':         # x1b is ESC
                break

finally:
    termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings)

For cross platform, or in case you want a GUI as well, you can use Pygame:对于跨平台,或者如果您还想要 GUI,您可以使用 Pygame:

import pygame
from pygame.locals import *

def display(str):
    text = font.render(str, True, (255, 255, 255), (159, 182, 205))
    textRect = text.get_rect()
    textRect.centerx = screen.get_rect().centerx
    textRect.centery = screen.get_rect().centery

    screen.blit(text, textRect)
    pygame.display.update()

pygame.init()
screen = pygame.display.set_mode( (640,480) )
pygame.display.set_caption('Python numbers')
screen.fill((159, 182, 205))

font = pygame.font.Font(None, 17)

num = 0
done = False
while not done:
    display( str(num) )
    num += 1

    pygame.event.pump()
    keys = pygame.key.get_pressed()
    if keys[K_ESCAPE]:
        done = True

This is the most awesome solution 1 I've ever seen.这是我见过的最棒的解决方案1 Pasted here in case link goes down:粘贴在这里以防链接失效:

#!/usr/bin/env python
'''
A Python class implementing KBHIT, the standard keyboard-interrupt poller.
Works transparently on Windows and Posix (Linux, Mac OS X).  Doesn't work
with IDLE.

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as 
published by the Free Software Foundation, either version 3 of the 
License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

'''

import os

# Windows
if os.name == 'nt':
    import msvcrt

# Posix (Linux, OS X)
else:
    import sys
    import termios
    import atexit
    from select import select


class KBHit:

    def __init__(self):
        '''Creates a KBHit object that you can call to do various keyboard things.
        '''

        if os.name == 'nt':
            pass

        else:

            # Save the terminal settings
            self.fd = sys.stdin.fileno()
            self.new_term = termios.tcgetattr(self.fd)
            self.old_term = termios.tcgetattr(self.fd)

            # New terminal setting unbuffered
            self.new_term[3] = (self.new_term[3] & ~termios.ICANON & ~termios.ECHO)
            termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.new_term)

            # Support normal-terminal reset at exit
            atexit.register(self.set_normal_term)


    def set_normal_term(self):
        ''' Resets to normal terminal.  On Windows this is a no-op.
        '''

        if os.name == 'nt':
            pass

        else:
            termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.old_term)


    def getch(self):
        ''' Returns a keyboard character after kbhit() has been called.
            Should not be called in the same program as getarrow().
        '''

        s = ''

        if os.name == 'nt':
            return msvcrt.getch().decode('utf-8')

        else:
            return sys.stdin.read(1)


    def getarrow(self):
        ''' Returns an arrow-key code after kbhit() has been called. Codes are
        0 : up
        1 : right
        2 : down
        3 : left
        Should not be called in the same program as getch().
        '''

        if os.name == 'nt':
            msvcrt.getch() # skip 0xE0
            c = msvcrt.getch()
            vals = [72, 77, 80, 75]

        else:
            c = sys.stdin.read(3)[2]
            vals = [65, 67, 66, 68]

        return vals.index(ord(c.decode('utf-8')))


    def kbhit(self):
        ''' Returns True if keyboard character was hit, False otherwise.
        '''
        if os.name == 'nt':
            return msvcrt.kbhit()

        else:
            dr,dw,de = select([sys.stdin], [], [], 0)
            return dr != []


# Test    
if __name__ == "__main__":

    kb = KBHit()

    print('Hit any key, or ESC to exit')

    while True:

        if kb.kbhit():
            c = kb.getch()
            if ord(c) == 27: # ESC
                break
            print(c)

    kb.set_normal_term()

1 Made by Simon D. Levy , part of a compilation of software he has written and released under the Gnu Lesser General Public License . 1Simon D. Levy制作,是他根据Gnu Lesser General Public License编写和发布的软件汇编的一部分。

Here a solution that runs under linux and windows using a seperate thread:这是一个使用单独线程在 linux 和 windows 下运行的解决方案:

import sys
import threading
import time
import Queue

def add_input(input_queue):
    while True:
        input_queue.put(sys.stdin.read(1))

def foobar():
    input_queue = Queue.Queue()

    input_thread = threading.Thread(target=add_input, args=(input_queue,))
    input_thread.daemon = True
    input_thread.start()

    last_update = time.time()
    while True:

        if time.time()-last_update>0.5:
            sys.stdout.write(".")
            last_update = time.time()

        if not input_queue.empty():
            print "\ninput:", input_queue.get()

foobar()

My favorite to get non-blocking input is using the python input() in a thread:我最喜欢获得非阻塞输入是在线程中使用 python input():

import threading

class KeyboardThread(threading.Thread):

    def __init__(self, input_cbk = None, name='keyboard-input-thread'):
        self.input_cbk = input_cbk
        super(KeyboardThread, self).__init__(name=name)
        self.start()

    def run(self):
        while True:
            self.input_cbk(input()) #waits to get input + Return

showcounter = 0 #something to demonstrate the change

def my_callback(inp):
    #evaluate the keyboard input
    print('You Entered:', inp, ' Counter is at:', showcounter)

#start the Keyboard thread
kthread = KeyboardThread(my_callback)

while True:
    #the normal program executes without blocking. here just counting up
    showcounter += 1

OS independent, only internal libraries, supports multi-character input独立于操作系统,仅内部库,支持多字符输入

On Linux, here's a refactoring of mizipzor's code that makes this a little easier, in case you have to use this code in multiple places.在 Linux 上,这里对 mizipzor 的代码进行了重构,这使得这更容易一些,以防您必须在多个地方使用此代码。

import sys
import select
import tty
import termios

class NonBlockingConsole(object):

    def __enter__(self):
        self.old_settings = termios.tcgetattr(sys.stdin)
        tty.setcbreak(sys.stdin.fileno())
        return self

    def __exit__(self, type, value, traceback):
        termios.tcsetattr(sys.stdin, termios.TCSADRAIN, self.old_settings)


    def get_data(self):
        if select.select([sys.stdin], [], [], 0) == ([sys.stdin], [], []):
            return sys.stdin.read(1)
        return False

Here's how to use this: This code will print a counter that keeps growing until you press ESC.以下是如何使用它:此代码将打印一个不断增长的计数器,直到您按下 ESC。

with NonBlockingConsole() as nbc:
    i = 0
    while 1:
        print i
        i += 1
        if nbc.get_data() == '\x1b':  # x1b is ESC
            break

I think curses library can help.我认为 curses 库可以提供帮助。

import curses
import datetime

stdscr = curses.initscr()
curses.noecho()
stdscr.nodelay(1) # set getch() non-blocking

stdscr.addstr(0,0,"Press \"p\" to show count, \"q\" to exit...")
line = 1
try:
    while 1:
        c = stdscr.getch()
        if c == ord('p'):
            stdscr.addstr(line,0,"Some text here")
            line += 1
        elif c == ord('q'): break

        """
        Do more things
        """

finally:
    curses.endwin()

.... backing to the initial question ... ....支持最初的问题...

i am learning python too, it cost me many documentation and examples readings and head crackings... but i think i reached an easy, simple, short and compatible solution... using just input, lists and threads我也在学习 python,它花费了我很多文档和示例阅读和头裂......但我认为我达到了一个简单、简单、简短和兼容的解决方案......只使用输入、列表和线程

'''
what i thought:
- input() in another thread
- that were filling a global strings list
- strings are being popped in the main thread
'''

import threading

consoleBuffer = []

def consoleInput(myBuffer):
  while True:
    myBuffer.append(input())
 
threading.Thread(target=consoleInput, args=(consoleBuffer,), daemon=True).start() # start the thread

import time # just to demonstrate non blocking parallel processing

while True:
  time.sleep(2) # avoid 100% cpu
  print(time.time()) # just to demonstrate non blocking parallel processing
  while consoleBuffer:
    print(repr(consoleBuffer.pop(0)))

until this is the simplest and compatible way i found, be aware by default stdin stdout and stderr share the same terminal so "local echo" of your input may look inconsistent if something is printed on console while you are typing, however after pressing enter the typed string is received well... if you don't want/like this behavior find a way to separate input/output areas like redirections, or try another solution like curses, tkinter, pygame, etc.直到这是我找到的最简单和兼容的方式,请注意默认情况下 stdin stdout 和 stderr 共享相同的终端,因此如果您在键入时在控制台上打印某些内容,则输入的“本地回显”可能看起来不一致,但是在按下回车后输入的字符串接收良好...如果您不希望/不喜欢这种行为,请找到一种方法来分隔输入/输出区域,如重定向,或尝试其他解决方案,如 curses、tkinter、pygame 等。

BONUS: the ctrl-c keystroke can be easily handled with奖励:可以轻松处理ctrl-c击键

try:
  # do whatever
except KeyboardInterrupt:
  print('cancelled by user') or exit() # overload

With python3.3 and above you can use the asyncio module as mentioned in this answer.使用 python3.3 及更高版本,您可以使用此答案中提到的asyncio模块。 You will have to re factor your code though to work with asyncio .您将不得不重新考虑您的代码以使用asyncio Prompt for user input using python asyncio.create_server instance 使用 python asyncio.create_server 实例提示用户输入

I'd do what Mickey Chan said, but I'd use unicurses instead of normal curses.我会按照 Mickey Chan 所说的去做,但我会使用unicurses而不是普通的诅咒。 Unicurses is universal (works on all or at least almost all operating systems) Unicurses是通用的(适用于所有或至少几乎所有操作系统)

Since I found one of the answers above helpful, here's an example of a similar approach.由于我发现上述答案之一很有帮助,因此这里有一个类似方法的示例。 This code creates a metronome effect while taking input.此代码在输入时创建节拍器效果。

The difference is this code uses a closure instead of a class, which feels a little more straight-forward to me.不同之处在于这段代码使用闭包而不是类,这对我来说感觉更直接一些。 This example also incorporates a flag to kill the thread via my_thread.stop = True , but without using a global variable.此示例还包含一个标志以通过my_thread.stop = True线程,但不使用全局变量。 I do this by (ab)using the fact that python functions are objects and thus can be monkey-patched, even from inside themselves.我通过(ab)使用python函数是对象的事实来做到这一点,因此可以进行猴子补丁,甚至可以从它们内部进行。

Note: Stopping threads should be done with caution.注意:停止线程应该小心。 If your thread has data that needs some kind of clean up process or if the thread spawned its own threads, this approach will unceremoniously kill those processes.如果您的线程有需要某种清理过程的数据,或者如果线程产生了自己的线程,这种方法将毫不客气地杀死这些进程。

# Begin metronome sound while accepting input.
# After pressing enter, turn off the metronome sound.
# Press enter again to restart the process.

import threading
import time
import winsound  # Only on Windows

beat_length = 1  # Metronome speed


def beat_thread():
    beat_thread.stop = False  # Monkey-patched flag
    frequency, duration = 2500, 10
    def run():  # Closure
        while not beat_thread.stop:  # Run until flag is True
            winsound.Beep(frequency, duration)
            time.sleep(beat_length - duration/1000)
    threading.Thread(target=run).start()


while True:
    beat_thread()
    input("Input with metronome. Enter to finish.\n")
    beat_thread.stop = True  # Flip monkey-patched flag
    input("Metronome paused. Enter to continue.\n\n")

If you just want a single "escape" from a loop, you can intercept the Ctrl-C signal.如果您只想从循环中“逃脱”一次,则可以拦截 Ctrl-C 信号。

This is cross-platform and very simple!这是跨平台的,非常简单!

import signal
import sys

def signal_handler(sig, frame):
    print('You pressed Ctrl+C!')
    sys.exit(0)

signal.signal(signal.SIGINT, signal_handler)
while True:
    # do your work here

The following is an class wrapper around one of the above solutions:以下是围绕上述解决方案之一的类包装器:

#!/usr/bin/env python3

import threading

import queue

class NonBlockingInput:

    def __init__(self, exit_condition):
        self.exit_condition = exit_condition
        self.input_queue = queue.Queue()
        self.input_thread = threading.Thread(target=self.read_kbd_input, args=(), daemon=True)
        self.input_thread.start()

    def read_kbd_input(self):
        done_queueing_input = False
        while not done_queueing_input:
            console_input = input()
            self.input_queue.put(console_input)
            if console_input.strip() == self.exit_condition:
                done_queueing_input = True

    def input_queued(self):
        return_value = False
        if self.input_queue.qsize() > 0:
            return_value = True
        return return_value

    def input_get(self):
        return_value = ""
        if self.input_queue.qsize() > 0:
            return_value = self.input_queue.get()
        return return_value

if __name__ == '__main__':

    NON_BLOCK_INPUT = NonBlockingInput(exit_condition='quit')

    DONE_PROCESSING = False
    INPUT_STR = ""
    while not DONE_PROCESSING:
        if NON_BLOCK_INPUT.input_queued():
            INPUT_STR = NON_BLOCK_INPUT.input_get()
            if INPUT_STR.strip() == "quit":
                DONE_PROCESSING = True
            else:
                print("{}".format(INPUT_STR))

I was writing a program using Linux that has a bigger mainloop that requires regular updates but also needs to read characters in a non-blocking way.我正在使用 Linux 编写一个程序,该程序具有更大的主循环,需要定期更新,但也需要以非阻塞方式读取字符。 But resetting the display, also loses the input buffer.但是重置显示,也会丢失输入缓冲区。 This is the solution that I came up with.这是我想出的解决方案。 Every time after the screen is updated it sets the terminal to non-blocking, waits for the mainloop to pass and then interprets stdin.每次屏幕更新后,它都会将终端设置为非阻塞,等待主循环通过,然后解释标准输入。 After that the terminal gets reset to the original settings.之后,终端将重置为原始设置。

#!/usr/bin/python3
import sys, select, os, tty, termios, time

i = 0
l = True
oldtty = termios.tcgetattr(sys.stdin)
stdin_no = sys.stdin.fileno()

while l:
    os.system('clear')
    print("I'm doing stuff. Press a 'q' to stop me!")
    print(i)
    tty.setcbreak(stdin_no)
    time.sleep(0.5)
    if sys.stdin in select.select([sys.stdin], [], [], 0.0)[0]:
        line = sys.stdin.read(1)
        print (line, len(line))
        
        if "q" in line:
            l = False
        else: 
            pass
    termios.tcsetattr(stdin_no, termios.TCSADRAIN, oldtty)
    i += 1


My example below does allow for non-blocking reads from stdin under both Windows (only tested under Windows 10) and Linux without requiring external dependencies or using threading.我下面的示例确实允许在 Windows(仅在 Windows 10 下测试)和 Linux 下从标准输入进行非阻塞读取,而无需外部依赖项或使用线程。 It works for copypasted text, it disables ECHO, so it could be used for eg some sort of custom UI and uses a loop, so it would be easy to process anything that was input into it.它适用于复制粘贴的文本,它禁用 ECHO,因此它可以用于例如某种自定义 UI 并使用循环,因此很容易处理输入到其中的任何内容。

With the above in mind, the example is meant for an interactive TTY, not piped input.考虑到上述情况,该示例适用于交互式 TTY,而不是管道输入。

#!/usr/bin/env python3
import sys

if(sys.platform == "win32"):
    import msvcrt
    import ctypes
    from ctypes import wintypes
    kernel32 = ctypes.windll.kernel32
    oldStdinMode = ctypes.wintypes.DWORD()
    # Windows standard handle -10 refers to stdin
    kernel32.GetConsoleMode(kernel32.GetStdHandle(-10), ctypes.byref(oldStdinMode))
    # Disable ECHO and line-mode
    # https://docs.microsoft.com/en-us/windows/console/setconsolemode
    kernel32.SetConsoleMode(kernel32.GetStdHandle(-10), 0)
else:
    # POSIX uses termios
    import select, termios, tty
    oldStdinMode = termios.tcgetattr(sys.stdin)
    _ = termios.tcgetattr(sys.stdin)
    # Disable ECHO and line-mode
    _[3] = _[3] & ~(termios.ECHO | termios.ICANON)
    # Don't block on stdin.read()
    _[6][termios.VMIN] = 0
    _[6][termios.VTIME] = 0
    termios.tcsetattr(sys.stdin, termios.TCSAFLUSH, _)

def readStdin():
    if(sys.platform == "win32"):
        return msvcrt.getwch() if(msvcrt.kbhit()) else ""
    else:
        return sys.stdin.read(1)

def flushStdin():
    if(sys.platform == "win32"):
        kernel32.FlushConsoleInputBuffer(kernel32.GetStdHandle(-10))
    else:
        termios.tcflush(sys.stdin, termios.TCIFLUSH)

try:
    userInput = ""
    print("Type something: ", end = "", flush = True)
    flushStdin()
    while 1:
        peek = readStdin()
        if(len(peek) > 0):
            # Stop input on NUL, Ctrl+C, ESC, carriage return, newline, backspace, EOF, EOT
            if(peek not in ["\0", "\3", "\x1b", "\r", "\n", "\b", "\x1a", "\4"]):
                userInput += peek
                # This is just to show the user what they typed.
                # Can be skipped, if one doesn't need this.
                sys.stdout.write(peek)
                sys.stdout.flush()
            else:
                break
    flushStdin()
    print(f"\nuserInput length: {len(userInput)}, contents: \"{userInput}\"")
finally:
    if(sys.platform == "win32"):
        kernel32.SetConsoleMode(kernel32.GetStdHandle(-10), oldStdinMode)
    else:
        termios.tcsetattr(sys.stdin, termios.TCSAFLUSH, oldStdinMode)

The Solution by marco is the right idea, but I decided to simplify it to the minimal possible code without any classes. marco 的解决方案是正确的想法,但我决定将其简化为尽可能少的代码,无需任何类。 Also it actually shows you how to get the user input with the queue library instead of just printing it:它实际上还向您展示了如何使用队列库获取用户输入,而不仅仅是打印它:

import time, threading, queue


def collect(que):
    msg = input()
    que.put(msg)

que = queue.Queue()
thread = threading.Thread(target=collect, args=[que])
thread.start()

while thread.is_alive():
    time.sleep(1)
    print("The main thread continues while we wait for you...")

msg = que.get()
print('You typed:', msg)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM