How to read a single character from the user?

后端 未结 23 2659
爱一瞬间的悲伤
爱一瞬间的悲伤 2020-11-21 04:28

Is there a way of reading one single character from the user input? For instance, they press one key at the terminal and it is returned (sort of like getch()).

相关标签:
23条回答
  • 2020-11-21 05:07

    The (currently) top-ranked answer (with the ActiveState code) is overly complicated. I don't see a reason to use classes when a mere function should suffice. Below are two implementations that accomplish the same thing but with more readable code.

    Both of these implementations:

    1. work just fine in Python 2 or Python 3
    2. work on Windows, OSX, and Linux
    3. read just one byte (i.e., they don't wait for a newline)
    4. don't depend on any external libraries
    5. are self-contained (no code outside of the function definition)

    Version 1: readable and simple

    def getChar():
        try:
            # for Windows-based systems
            import msvcrt # If successful, we are on Windows
            return msvcrt.getch()
    
        except ImportError:
            # for POSIX-based systems (with termios & tty support)
            import tty, sys, termios  # raises ImportError if unsupported
    
            fd = sys.stdin.fileno()
            oldSettings = termios.tcgetattr(fd)
    
            try:
                tty.setcbreak(fd)
                answer = sys.stdin.read(1)
            finally:
                termios.tcsetattr(fd, termios.TCSADRAIN, oldSettings)
    
            return answer
    

    Version 2: avoid repeated imports and exception handling:

    [EDIT] I missed one advantage of the ActiveState code. If you plan to read characters multiple times, that code avoids the (negligible) cost of repeating the Windows import and the ImportError exception handling on Unix-like systems. While you probably should be more concerned about code readability than that negligible optimization, here is an alternative (it is similar to Louis's answer, but getChar() is self-contained) that functions the same as the ActiveState code and is more readable:

    def getChar():
        # figure out which function to use once, and store it in _func
        if "_func" not in getChar.__dict__:
            try:
                # for Windows-based systems
                import msvcrt # If successful, we are on Windows
                getChar._func=msvcrt.getch
    
            except ImportError:
                # for POSIX-based systems (with termios & tty support)
                import tty, sys, termios # raises ImportError if unsupported
    
                def _ttyRead():
                    fd = sys.stdin.fileno()
                    oldSettings = termios.tcgetattr(fd)
    
                    try:
                        tty.setcbreak(fd)
                        answer = sys.stdin.read(1)
                    finally:
                        termios.tcsetattr(fd, termios.TCSADRAIN, oldSettings)
    
                    return answer
    
                getChar._func=_ttyRead
    
        return getChar._func()
    

    Example code that exercises either of the getChar() versions above:

    from __future__ import print_function # put at top of file if using Python 2
    
    # Example of a prompt for one character of input
    promptStr   = "Please give me a character:"
    responseStr = "Thank you for giving me a '{}'."
    print(promptStr, end="\n> ")
    answer = getChar()
    print("\n")
    print(responseStr.format(answer))
    
    0 讨论(0)
  • 2020-11-21 05:10

    The answers here were informative, however I also wanted a way to get key presses asynchronously and fire off key presses in separate events, all in a thread-safe, cross-platform way. PyGame was also too bloated for me. So I made the following (in Python 2.7 but I suspect it's easily portable), which I figured I'd share here in case it was useful for anyone else. I stored this in a file named keyPress.py.

    class _Getch:
        """Gets a single character from standard input.  Does not echo to the
    screen. From http://code.activestate.com/recipes/134892/"""
        def __init__(self):
            try:
                self.impl = _GetchWindows()
            except ImportError:
                try:
                    self.impl = _GetchMacCarbon()
                except(AttributeError, ImportError):
                    self.impl = _GetchUnix()
    
        def __call__(self): return self.impl()
    
    
    class _GetchUnix:
        def __init__(self):
            import tty, sys, termios # import termios now or else you'll get the Unix version on the Mac
    
        def __call__(self):
            import sys, tty, termios
            fd = sys.stdin.fileno()
            old_settings = termios.tcgetattr(fd)
            try:
                tty.setraw(sys.stdin.fileno())
                ch = sys.stdin.read(1)
            finally:
                termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
            return ch
    
    class _GetchWindows:
        def __init__(self):
            import msvcrt
    
        def __call__(self):
            import msvcrt
            return msvcrt.getch()
    
    class _GetchMacCarbon:
        """
        A function which returns the current ASCII key that is down;
        if no ASCII key is down, the null string is returned.  The
        page http://www.mactech.com/macintosh-c/chap02-1.html was
        very helpful in figuring out how to do this.
        """
        def __init__(self):
            import Carbon
            Carbon.Evt #see if it has this (in Unix, it doesn't)
    
        def __call__(self):
            import Carbon
            if Carbon.Evt.EventAvail(0x0008)[0]==0: # 0x0008 is the keyDownMask
                return ''
            else:
                #
                # The event contains the following info:
                # (what,msg,when,where,mod)=Carbon.Evt.GetNextEvent(0x0008)[1]
                #
                # The message (msg) contains the ASCII char which is
                # extracted with the 0x000000FF charCodeMask; this
                # number is converted to an ASCII character with chr() and
                # returned
                #
                (what,msg,when,where,mod)=Carbon.Evt.GetNextEvent(0x0008)[1]
                return chr(msg & 0x000000FF)
    
    import threading
                
                
    # From  https://stackoverflow.com/a/2022629/2924421
    class Event(list):
        def __call__(self, *args, **kwargs):
            for f in self:
                f(*args, **kwargs)
    
        def __repr__(self):
            return "Event(%s)" % list.__repr__(self)            
    
    
    def getKey():
        inkey = _Getch()
        import sys
        for i in xrange(sys.maxint):
            k=inkey()
            if k<>'':break
        return k
    
    class KeyCallbackFunction():
        callbackParam = None
        actualFunction = None
        
        def __init__(self, actualFunction, callbackParam):
            self.actualFunction = actualFunction
            self.callbackParam = callbackParam
    
        def doCallback(self, inputKey):
            if not self.actualFunction is None:
                if self.callbackParam is None:
                    callbackFunctionThread = threading.Thread(target=self.actualFunction, args=(inputKey,))
                else:
                    callbackFunctionThread = threading.Thread(target=self.actualFunction, args=(inputKey,self.callbackParam))
                
                callbackFunctionThread.daemon = True
                callbackFunctionThread.start()
            
            
    
    class KeyCapture():
    
    
        gotKeyLock = threading.Lock()
        gotKeys = []
        gotKeyEvent = threading.Event()
    
        keyBlockingSetKeyLock = threading.Lock()
    
        addingEventsLock = threading.Lock()
        keyReceiveEvents = Event()
    
    
        keysGotLock = threading.Lock()
        keysGot = []
    
        keyBlockingKeyLockLossy = threading.Lock()
        keyBlockingKeyLossy = None
        keyBlockingEventLossy = threading.Event()
        
        keysBlockingGotLock = threading.Lock()
        keysBlockingGot = []
        keyBlockingGotEvent = threading.Event()
        
    
        
        wantToStopLock = threading.Lock()
        wantToStop = False
        
        stoppedLock = threading.Lock()
        stopped = True
        
        isRunningEvent = False
        
        getKeyThread = None
        
        keyFunction = None
        keyArgs = None
        
        # Begin capturing keys. A seperate thread is launched that
        # captures key presses, and then these can be received via get,
        # getAsync, and adding an event via addEvent. Note that this
        # will prevent the system to accept keys as normal (say, if
        # you are in a python shell) because it overrides that key
        # capturing behavior.
        
        # If you start capture when it's already been started, a
        # InterruptedError("Keys are still being captured")
        # will be thrown
        
        # Note that get(), getAsync() and events are independent, so if a key is pressed:
        #
        # 1: Any calls to get() that are waiting, with lossy on, will return
        #    that key
        # 2: It will be stored in the queue of get keys, so that get() with lossy
        #    off will return the oldest key pressed not returned by get() yet.
        # 3: All events will be fired with that key as their input
        # 4: It will be stored in the list of getAsync() keys, where that list
        #    will be returned and set to empty list on the next call to getAsync().
        # get() call with it, aand add it to the getAsync() list.
        def startCapture(self, keyFunction=None, args=None):
            # Make sure we aren't already capturing keys
            self.stoppedLock.acquire()
            if not self.stopped:
                self.stoppedLock.release()
                raise InterruptedError("Keys are still being captured")
                return
            self.stopped = False
            self.stoppedLock.release()
            
            # If we have captured before, we need to allow the get() calls to actually
            # wait for key presses now by clearing the event
            if self.keyBlockingEventLossy.is_set():
                self.keyBlockingEventLossy.clear()
                
            # Have one function that we call every time a key is captured, intended for stopping capture
            # as desired
            self.keyFunction = keyFunction
            self.keyArgs = args
            
            # Begin capturing keys (in a seperate thread)
            self.getKeyThread = threading.Thread(target=self._threadProcessKeyPresses)
            self.getKeyThread.daemon = True
            self.getKeyThread.start()
            
            # Process key captures (in a seperate thread)
            self.getKeyThread = threading.Thread(target=self._threadStoreKeyPresses)
            self.getKeyThread.daemon = True
            self.getKeyThread.start()
        
        
        def capturing(self):
            self.stoppedLock.acquire()
            isCapturing = not self.stopped
            self.stoppedLock.release()
            return isCapturing
        # Stops the thread that is capturing keys on the first opporunity
        # has to do so. It usually can't stop immediately because getting a key
        # is a blocking process, so this will probably stop capturing after the
        # next key is pressed.
        #
        # However, Sometimes if you call stopCapture it will stop before starting capturing the
        # next key, due to multithreading race conditions. So if you want to stop capturing
        # reliably, call stopCapture in a function added via addEvent. Then you are
        # guaranteed that capturing will stop immediately after the rest of the callback
        # functions are called (before starting to capture the next key).
        def stopCapture(self):
            self.wantToStopLock.acquire()
            self.wantToStop = True 
            self.wantToStopLock.release()
    
        # Takes in a function that will be called every time a key is pressed (with that
        # key passed in as the first paramater in that function)
        def addEvent(self, keyPressEventFunction, args=None):   
            self.addingEventsLock.acquire()
            callbackHolder = KeyCallbackFunction(keyPressEventFunction, args)
            self.keyReceiveEvents.append(callbackHolder.doCallback)
            self.addingEventsLock.release()
        def clearEvents(self):
            self.addingEventsLock.acquire()
            self.keyReceiveEvents = Event()
            self.addingEventsLock.release()
        # Gets a key captured by this KeyCapture, blocking until a key is pressed.
        # There is an optional lossy paramater:
        # If True all keys before this call are ignored, and the next pressed key
        #   will be returned.
        # If False this will return the oldest key captured that hasn't
        #   been returned by get yet. False is the default.
        def get(self, lossy=False):
            if lossy:
                # Wait for the next key to be pressed
                self.keyBlockingEventLossy.wait()
                self.keyBlockingKeyLockLossy.acquire()
                keyReceived = self.keyBlockingKeyLossy
                self.keyBlockingKeyLockLossy.release()
                return keyReceived
            else:
                while True:
                    # Wait until a key is pressed
                    self.keyBlockingGotEvent.wait()
                    
                    # Get the key pressed
                    readKey = None
                    self.keysBlockingGotLock.acquire()
                    # Get a key if it exists
                    if len(self.keysBlockingGot) != 0:
                        readKey = self.keysBlockingGot.pop(0)
                    # If we got the last one, tell us to wait
                    if len(self.keysBlockingGot) == 0:
                        self.keyBlockingGotEvent.clear()
                    self.keysBlockingGotLock.release()
                    
                    # Process the key (if it actually exists)
                    if not readKey is None:
                        return readKey
                    
                    # Exit if we are stopping
                    self.wantToStopLock.acquire()
                    if self.wantToStop:
                        self.wantToStopLock.release()
                        return None
                    self.wantToStopLock.release()
                
                
                
        
        def clearGetList(self):
            self.keysBlockingGotLock.acquire()
            self.keysBlockingGot = []
            self.keysBlockingGotLock.release()
        
        # Gets a list of all keys pressed since the last call to getAsync, in order
        # from first pressed, second pressed, .., most recent pressed
        def getAsync(self):
            self.keysGotLock.acquire();
            keysPressedList = list(self.keysGot)
            self.keysGot = []
            self.keysGotLock.release()
            return keysPressedList
        
        def clearAsyncList(self):
            self.keysGotLock.acquire();
            self.keysGot = []
            self.keysGotLock.release();
    
        def _processKey(self, readKey):
            # Append to list for GetKeyAsync
            self.keysGotLock.acquire()
            self.keysGot.append(readKey)
            self.keysGotLock.release()
            
            # Call lossy blocking key events
            self.keyBlockingKeyLockLossy.acquire()
            self.keyBlockingKeyLossy = readKey
            self.keyBlockingEventLossy.set()
            self.keyBlockingEventLossy.clear()
            self.keyBlockingKeyLockLossy.release()
            
            # Call non-lossy blocking key events
            self.keysBlockingGotLock.acquire()
            self.keysBlockingGot.append(readKey)
            if len(self.keysBlockingGot) == 1:
                self.keyBlockingGotEvent.set()
            self.keysBlockingGotLock.release()
            
            # Call events added by AddEvent
            self.addingEventsLock.acquire()
            self.keyReceiveEvents(readKey)
            self.addingEventsLock.release()
    
        def _threadProcessKeyPresses(self):
            while True:
                # Wait until a key is pressed
                self.gotKeyEvent.wait()
                
                # Get the key pressed
                readKey = None
                self.gotKeyLock.acquire()
                # Get a key if it exists
                if len(self.gotKeys) != 0:
                    readKey = self.gotKeys.pop(0)
                # If we got the last one, tell us to wait
                if len(self.gotKeys) == 0:
                    self.gotKeyEvent.clear()
                self.gotKeyLock.release()
                
                # Process the key (if it actually exists)
                if not readKey is None:
                    self._processKey(readKey)
                
                # Exit if we are stopping
                self.wantToStopLock.acquire()
                if self.wantToStop:
                    self.wantToStopLock.release()
                    break
                self.wantToStopLock.release()
                
        def _threadStoreKeyPresses(self):
            while True:
                # Get a key
                readKey = getKey()
                
                # Run the potential shut down function
                if not self.keyFunction is None:
                    self.keyFunction(readKey, self.keyArgs)
            
                # Add the key to the list of pressed keys
                self.gotKeyLock.acquire()
                self.gotKeys.append(readKey)
                if len(self.gotKeys) == 1:
                    self.gotKeyEvent.set()
                self.gotKeyLock.release()
                
                # Exit if we are stopping
                self.wantToStopLock.acquire()
                if self.wantToStop:
                    self.wantToStopLock.release()
                    self.gotKeyEvent.set()
                    break
                self.wantToStopLock.release()
        
            
            # If we have reached here we stopped capturing
            
            # All we need to do to clean up is ensure that
            # all the calls to .get() now return None.
            # To ensure no calls are stuck never returning,
            # we will leave the event set so any tasks waiting
            # for it immediately exit. This will be unset upon
            # starting key capturing again.
            
            self.stoppedLock.acquire()
            
            # We also need to set this to True so we can start up
            # capturing again.
            self.stopped = True
            self.stopped = True
            
            self.keyBlockingKeyLockLossy.acquire()
            self.keyBlockingKeyLossy = None
            self.keyBlockingEventLossy.set()
            self.keyBlockingKeyLockLossy.release()
            
            self.keysBlockingGotLock.acquire()
            self.keyBlockingGotEvent.set()
            self.keysBlockingGotLock.release()
            
            self.stoppedLock.release()
    

    The idea is that you can either simply call keyPress.getKey(), which will read a key from the keyboard, then return it.

    If you want something more than that, I made a KeyCapture object. You can create one via something like keys = keyPress.KeyCapture().

    Then there are three things you can do:

    addEvent(functionName) takes in any function that takes in one parameter. Then every time a key is pressed, this function will be called with that key's string as it's input. These are ran in a separate thread, so you can block all you want in them and it won't mess up the functionality of the KeyCapturer nor delay the other events.

    get() returns a key in the same blocking way as before. It is now needed here because the keys are being captured via the KeyCapture object now, so keyPress.getKey() would conflict with that behavior and both of them would miss some keys since only one key can be captured at a time. Also, say the user presses 'a', then 'b', you call get(), the user presses 'c'. That get() call will immediately return 'a', then if you call it again it will return 'b', then 'c'. If you call it again it will block until another key is pressed. This ensures that you don't miss any keys, in a blocking way if desired. So in this way it's a little different than keyPress.getKey() from before

    If you want the behavior of getKey() back, get(lossy=True) is like get(), except that it only returns keys pressed after the call to get(). So in the above example, get() would block until the user presses 'c', and then if you call it again it will block until another key is pressed.

    getAsync() is a little different. It's designed for something that does a lot of processing, then occasionally comes back and checks which keys were pressed. Thus getAsync() returns a list of all the keys pressed since the last call to getAsync(), in order from oldest key pressed to most recent key pressed. It also doesn't block, meaning that if no keys have been pressed since the last call to getAsync(), an empty [] will be returned.

    To actually start capturing keys, you need to call keys.startCapture() with your keys object made above. startCapture is non-blocking, and simply starts one thread that just records the key presses, and another thread to process those key presses. There are two threads to ensure that the thread that records key presses doesn't miss any keys.

    If you want to stop capturing keys, you can call keys.stopCapture() and it will stop capturing keys. However, since capturing a key is a blocking operation, the thread capturing keys might capture one more key after calling stopCapture().

    To prevent this, you can pass in an optional parameter(s) into startCapture(functionName, args) of a function that just does something like checks if a key equals 'c' and then exits. It's important that this function does very little before, for example, a sleep here will cause us to miss keys.

    However, if stopCapture() is called in this function, key captures will be stopped immediately, without trying to capture any more, and that all get() calls will be returned immediately, with None if no keys have been pressed yet.

    Also, since get() and getAsync() store all the previous keys pressed (until you retrieve them), you can call clearGetList() and clearAsyncList() to forget the keys previously pressed.

    Note that get(), getAsync() and events are independent, so if a key is pressed:

    1. One call to get() that is waiting, with lossy on, will return that key. The other waiting calls (if any) will continue waiting.
    2. That key will be stored in the queue of get keys, so that get() with lossy off will return the oldest key pressed not returned by get() yet.
    3. All events will be fired with that key as their input
    4. That key will be stored in the list of getAsync() keys, where that lis twill be returned and set to empty list on the next call to getAsync()

    If all this is too much, here is an example use case:

    import keyPress
    import time
    import threading
    
    def KeyPressed(k, printLock):
        printLock.acquire()
        print "Event: " + k
        printLock.release()
        time.sleep(4)
        printLock.acquire()
        print "Event after delay: " + k
        printLock.release()
    
    def GetKeyBlocking(keys, printLock):    
        while keys.capturing():
            keyReceived = keys.get()
            time.sleep(1)
            printLock.acquire()
            if not keyReceived is None:
                print "Block " + keyReceived
            else:
                print "Block None"
            printLock.release()
    
    def GetKeyBlockingLossy(keys, printLock):   
        while keys.capturing():
            keyReceived = keys.get(lossy=True)
            time.sleep(1)
            printLock.acquire()
            if not keyReceived is None:
                print "Lossy: " + keyReceived
            else:
                print "Lossy: None"
            printLock.release()
    
    def CheckToClose(k, (keys, printLock)):
        printLock.acquire()
        print "Close: " + k
        printLock.release()
        if k == "c":
            keys.stopCapture()
            
    printLock = threading.Lock()
    
    print "Press a key:"
    print "You pressed: " + keyPress.getKey()
    print ""
    
    keys = keyPress.KeyCapture()
    
    keys.addEvent(KeyPressed, printLock)
    
    
    
    print "Starting capture"
                
    keys.startCapture(CheckToClose, (keys, printLock))
                
    getKeyBlockingThread = threading.Thread(target=GetKeyBlocking, args=(keys, printLock))
    getKeyBlockingThread.daemon = True
    getKeyBlockingThread.start()
    
                
    getKeyBlockingThreadLossy = threading.Thread(target=GetKeyBlockingLossy, args=(keys, printLock))
    getKeyBlockingThreadLossy.daemon = True
    getKeyBlockingThreadLossy.start()
    
    while keys.capturing():
        keysPressed = keys.getAsync()
        printLock.acquire()
        if keysPressed != []:
            print "Async: " + str(keysPressed)
        printLock.release()
        time.sleep(1)
    
    print "done capturing"
    

    It is working well for me from the simple test I made, but I will happily take others feedback as well if there is something I missed.

    I posted this here as well.

    0 讨论(0)
  • 2020-11-21 05:10

    The accepted answer didn't perform that well for me (I'd hold a key, nothing would happen, then I'd press another key and it would work).

    After learning about the curses module, it really seems like the right way to go. And it's now available for Windows through windows-cursors (available through pip), so you can program in a platform agnostic manner. Here's an example inspired by this nice tutorial on YouTube:

    import curses                                                                                                                                       
    def getkey(stdscr):
        curses.curs_set(0)
        while True:
            key = stdscr.getch()
            if key != -1:
                break
        return key
    
    if __name__ == "__main__":
        print(curses.wrapper(getkey))
    

    Save it with a .py extension, or run curses.wrapper(getkey) in interactive mode.

    0 讨论(0)
  • 2020-11-21 05:11

    A comment in one of the other answers mentioned cbreak mode, which is important for Unix implementations because you generally don't want ^C (KeyboardError) to be consumed by getchar (as it will when you set the terminal to raw mode, as done by most other answers).

    Another important detail is that if you're looking to read one character and not one byte, you should read 4 bytes from the input stream, as that's the maximum number of bytes a single character will consist of in UTF-8 (Python 3+). Reading only a single byte will produce unexpected results for multi-byte characters such as keypad arrows.

    Here's my changed implementation for Unix:

    import contextlib
    import os
    import sys
    import termios
    import tty
    
    
    _MAX_CHARACTER_BYTE_LENGTH = 4
    
    
    @contextlib.contextmanager
    def _tty_reset(file_descriptor):
        """
        A context manager that saves the tty flags of a file descriptor upon
        entering and restores them upon exiting.
        """
        old_settings = termios.tcgetattr(file_descriptor)
        try:
            yield
        finally:
            termios.tcsetattr(file_descriptor, termios.TCSADRAIN, old_settings)
    
    
    def get_character(file=sys.stdin):
        """
        Read a single character from the given input stream (defaults to sys.stdin).
        """
        file_descriptor = file.fileno()
        with _tty_reset(file_descriptor):
            tty.setcbreak(file_descriptor)
            return os.read(file_descriptor, _MAX_CHARACTER_BYTE_LENGTH)
    
    0 讨论(0)
  • 2020-11-21 05:12

    This code, based off here, will correctly raise KeyboardInterrupt and EOFError if Ctrl+C or Ctrl+D are pressed.

    Should work on Windows and Linux. An OS X version is available from the original source.

    class _Getch:
        """Gets a single character from standard input.  Does not echo to the screen."""
        def __init__(self):
            try:
                self.impl = _GetchWindows()
            except ImportError:
                self.impl = _GetchUnix()
    
        def __call__(self): 
            char = self.impl()
            if char == '\x03':
                raise KeyboardInterrupt
            elif char == '\x04':
                raise EOFError
            return char
    
    class _GetchUnix:
        def __init__(self):
            import tty
            import sys
    
        def __call__(self):
            import sys
            import tty
            import termios
            fd = sys.stdin.fileno()
            old_settings = termios.tcgetattr(fd)
            try:
                tty.setraw(sys.stdin.fileno())
                ch = sys.stdin.read(1)
            finally:
                termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
            return ch
    
    
    class _GetchWindows:
        def __init__(self):
            import msvcrt
    
        def __call__(self):
            import msvcrt
            return msvcrt.getch()
    
    
    getch = _Getch()
    
    0 讨论(0)
  • 2020-11-21 05:12

    Try this with pygame:

    import pygame
    pygame.init()             // eliminate error, pygame.error: video system not initialized
    keys = pygame.key.get_pressed()
    
    if keys[pygame.K_SPACE]:
        d = "space key"
    
    print "You pressed the", d, "."
    
    0 讨论(0)
提交回复
热议问题