Python: Tastatureingaben erfassen

  Ralf Hersel   Lesezeit: 3 Minuten  🗪 11 Kommentare Auf Mastodon ansehen

Eine Schleife durch einen Tastendruck zu unterbrechen, ist in Python gar nicht so einfach. Doch die KI weiss, wie es geht.

python: tastatureingaben erfassen

Möchtet ihr die längliche Herleitung lesen? Hier ist sie: Bei GNU/Linux.ch gibt es Wettbewerbe, bei denen man tolle Preise gewinnen kann. In letzter Zeit waren das Schreibwettbewerbe, die die Community zum Mitmachen bewegen sollten. Wir planen jedoch wieder einen Programmierwettbewerb. Ein Solcher bedarf einer gründlichen Planung, damit die Aufgabe interessant und nicht zu schwierig ist. Damit beschäftige ich mich zurzeit.

Selbstverständlich verrate ich nicht, worum es darin gehen wird. Nur soviel, es wird ein lustiges Partyspiel sein, welches es zu programmieren gilt. Ein Teil dieses Spiels ist ein Countdown-Zähler, der mit einem Tastendruck unterbrochen werden soll. Und damit komme ich zum Inhalt dieses Artikels.

Die Frage lautet: Wie kann ich eine laufende Schleife in Python durch einen beliebigen Tastendruck unterbrechen? Das klingt simple, ist es aber nicht. Beginnen möchte ich mit einem Schleifen-Beispiel in Python:

#!/usr/bin/env python3

import time

number = 10

while True:
    if number == 0:
        break
    else:
        print(number)
        number -= 1
        time.sleep(1)

Was macht dieses Skript? Es importiert das Modul time, damit man die Schleife um 1 Sekunde verlangsamen kann. Es definiert die Variable number, die in der while-Schleife heruntergezählt wird. Bei jedem Durchlauf der Schleife wird der Wert von number ausgegeben. Sobald number bis auf Null heruntergezählt ist, wird die Schleife mit break abgebrochen. So weit, so einfach.

Nun kommt der schwierige Teil. Die Schleife soll durch einen beliebigen Tastendruck unterbrochen werden können. Eine Suche im Internet fördert nur umständliche Lösungen hervor, die Zusatzmodule verwenden, die nicht zu den Python-Standardmodulen gehören, z. B. das Modul keyboard oder pynput. Ich möchte hier nicht darauf eingehen, warum diese Module etwas nervig sind.

Eine bessere Lösung habe ich nicht mit der Suchmaschine, sondern mit GPT 3.5-Turbo gefunden. Am Anfang der Fragerunde präsentierte die KI dieselben Lösungsvorschläge, wie die normale Suche. Nachdem ich einige Male nachgefragt habe, rückte die KI eine bessere Lösung heraus:

import sys
import select

run_loop = True

while run_loop:
    rlist, _, _ = select.select([sys.stdin], [], [], 0.1)
    if rlist:
        key = sys.stdin.read(1)
        if key:
            run_loop = False

    # Your loop code here
    pass

Solchen KI-Vorschlägen darf man nie blind vertrauen, insbesondere wenn es um Code geht. Daher habe ich nachgelesen, was die Methode select im Modul select macht. Alles gut; keine Halluzinationen; funktioniert.

Und so sieht das Ergebnis aus:

#!/usr/bin/env python3

import time
import sys
import select

number = 10

while True:
    rlist, _, _ = select.select([sys.stdin], [], [], 0.1)
    if rlist:
        key = sys.stdin.read(1)
        if key:
            break
    if number == 0:
        break
    else:
        print(number)
        number -= 1
        time.sleep(1)

Das ist die einfachste Lösung, die ich finden konnte, um eine Schleife (non-blocking) durch einen Tastendruck abzubrechen. Falls ihr etwas Besseres kennt, nur her damit.

Tags

Python, Loop, Keystroke, Schleife, unterbrechen

fleixi
Geschrieben von fleixi am 3. Mai 2024 um 10:50

Dumme Frage aber warum “while = True“ und nicht “while number > -1“? Dann spart man sich die komplette "if number == 0"

efbe
Geschrieben von efbe am 3. Mai 2024 um 14:40

Oder noch kürzer 'while number:'

efbe
Geschrieben von efbe am 3. Mai 2024 um 14:44

Bei mir (Tuxedo os 2, KDE, Python 3.10.12) klappt es nur mit der entertaste, nicht mit normalen.

Ralf Hersel Admin
Geschrieben von Ralf Hersel am 3. Mai 2024 um 20:07

Kann ist bestätigen. Verstehe jedoch nicht, warum der Code nur auf ENTER reagiert.

Ralf Hersel Admin
Geschrieben von Ralf Hersel am 3. Mai 2024 um 20:05

Im Artikel geht es um "non-blocking keystroke detection", nicht um schönen Code des Drumherums. Ausserdem geht es um Verständlichkeit für Einsteieger:innen.

Claudia
Geschrieben von Claudia am 3. Mai 2024 um 15:24

Ich habe Deinen Ansatz gefunden und noch zwei weitere "Signal" und "Thread". (GitHub: atupal/select_input.py)[https://gist.github.com/atupal/5865237]

Ralf Hersel Admin
Geschrieben von Ralf Hersel am 3. Mai 2024 um 20:13

Danke für die Beispiele. Aber sie sind "blocking" und reagieren auch nur auf ENTER.

Roland
Geschrieben von Roland am 3. Mai 2024 um 16:14

Funktioniert das auch auf Windows?

Ralf Hersel Admin
Geschrieben von Ralf Hersel am 3. Mai 2024 um 20:14

Probiere es aus. Ich weiss es nicht.

Claudia
Geschrieben von Claudia am 4. Mai 2024 um 10:04

Ich habe nun die perfekte Lösung gefunden, die bei jedes Zeichen auslöst, aber die Schwierigkeit ist folgende: "Es muss genau in den richtigen moment die Taste gedrückt werden. Bei 0.1 war es mir unmöglich selbst bei 2 ist es schwierig. Nur wenn ich eine Taste spamme, dann löst es aus und beendet sich."

Code mit normalen Python Bibliotheken ohne etwas per pip nach zu installieren. Disclaimer: ABER funktioniert nur unter Linux! Windows 10/11 könnte es vielleicht per WSL funktionieren, aber ist nicht getestet, da ich dies nicht einsetze.

Angepasster Code:

import time
import sys
import termios
import tty
import select

def readchar():
    fd = sys.stdin.fileno()
    old_settings = termios.tcgetattr(fd)
    ch = -1
    try:
        tty.setraw(sys.stdin.fileno())
        rlist, _, _ = select.select([sys.stdin], [], [], 2)
        if rlist:
            ch = sys.stdin.read(1)
    finally:
        termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
    return ch

number = 10

while True:
    key = readchar()
    if key != -1:
        break
    if number == 0:
        break
    else:
        print(number)
        number -= 1
        time.sleep(1)
kamome
Geschrieben von kamome am 5. Mai 2024 um 19:40

Wie läuft das hier noch mal mit dem Code? Mit tripple-backtick-fences?

#!/usr/bin/env python

"""
non-block-input-countdown.py

Countdown, possible to interrupt with any key.

Found possible partial solution via _searx_ with
`python read single character non-blocking` on
https://shallowsky.com/blog/programming/python-read-characters.html
but had to be adapted (was Python 2, and shell needed a 'blind' `reset`),
and didn't work (for me) with a countdown. But it did link to a better (partial)
solution on
https://ballingt.com/nonblocking-stdin-in-python-3/
Adapted that - working.

Something with `Queue` und `Thread` also looked promissing …
"""

import fcntl
import os
import sys
import termios
import time
import tty

class raw(object):
        def __init__(self, stream):
                self.stream = stream
                self.fd = self.stream.fileno()
        def __enter__(self):
                self.original_stty = termios.tcgetattr(self.stream)
                tty.setcbreak(self.stream)
        def __exit__(self, type, value, traceback):
            termios.tcsetattr(self.stream, termios.TCSANOW, self.original_stty)

class nonblocking(object):
        def __init__(self, stream):
                self.stream = stream
                self.fd = self.stream.fileno()
        def __enter__(self):
                self.orig_fl = fcntl.fcntl(self.fd, fcntl.F_GETFL)
                fcntl.fcntl(self.fd, fcntl.F_SETFL, self.orig_fl | os.O_NONBLOCK)
        def __exit__(self, *args):
            fcntl.fcntl(self.fd, fcntl.F_SETFL, self.orig_fl)

countdown = 3
c = ''

with raw(sys.stdin):
        with nonblocking(sys.stdin):
                while countdown and not c:
                        print(countdown)
                        time.sleep(1)
                        c = sys.stdin.read(1)
                        countdown -= 1