Seid froh, dass Python eine blockierende input-Funktion hat

Alles, was nicht direkt mit Python-Problemen zu tun hat. Dies ist auch der perfekte Platz für Jobangebote.
Antworten
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

[ich hatte mal wieder Lust auf einen "Aufsatz"...]

Seid froh, dass Python eine "blockierende" `input`-Funktion hat.

Vor zwei Jahren hatte ich mal einen Basic-Interpreter für den Spieleklassiker Hammurabi in Python gebaut. Das Spiel funktioniert in der Konsole, stellt dem Benutzer fragen, erwartet seine Antwort per `input` und verarbeitet sie entsprechend. Nichts besonderes sollte man meinen.

Hier ist ein simples Beispiel für ein Spiel, das in einer Schleife Eingaben verarbeitet:

Code: Alles auswählen

    def zahlenraten(n):
        while True:
    	    z = input("Rate die Zahl: ")
    	    if n == z: print "Richtig!"; return
    	    print "Die Zahl ist", "kleiner" if n < z else "größer"
Nun wollte ich meinen Basic-Interpreter in JavaScript nachbauen. Weder Node.js noch der Browser haben jedoch ein blockierendes "input". Stattdessen muss ich einen "Listener" registrieren, eine Funktion, die aufgerufen wird, wenn ich eine Eingabe habe.

Nennen wir diese Funktion `async_input`. In Python könnte ich sie so simulieren:

Code: Alles auswählen

    def async_input(prompt, callback):
        callback(input(prompt))
Wie das in JavaScript aussieht? Gut, dass ihr fragt, ich hätte so viel JavaScript sonst nicht in einem Python-Forum geschrieben :) Ich habe mir das folgende Node.js-Modul `read.js` definiert, welches eine Funktion `read` exportiert, die genauso wie `async_input` funktioniert:

Code: Alles auswählen

    var buffer = "", pending = null, eof = false;
    
    // by default, encoding is binary but we want unicode strings
    process.stdin.setEncoding('utf8');
    
    // collect chunks of data (typically lines, but nobody guarantees that)
    process.stdin.on('data', function (data) {
        buffer += data;
        if (pending) read(pending);
    });
    
    // somebody pressed ^D or if stdin has been redirected, EOF
    process.stdin.on('end', function () {
        buffer += '\n';
        if (pending) read(pending);
        eof = true;
    });
    
    // by default, the stream is paused, so after setting up, resume it
    process.stdin.resume();
    
    /**
     * Reads the next line from stdin.
     * @param {function(string|null)} callback
     */
    function read(callback) {
        if (eof) return callback(null);
        var i = buffer.indexOf('\n');
        if (i !== -1) {
            var line = buffer.slice(0, i);
            buffer = buffer.slice(i + 1);
            pending = null;
            callback(line);
        } else {
            pending = callback;
        }
    }
    
    module.exports = read;
Zurück zu Python. Wenn ich `input` durch `async_input` ersetzen will, muss ich mein ganzes Programm einer CSP-Transformation unterwerfen. Das das genau bedeutet will ich gar nicht ausführen, die Daumenregel ist, dass ich alles, was `input` folgt, in eine Funktion auslagern muss, die ich dann `async_input` übergebe. Dazu muss ich zunächst einmal die `while`-Schleife loswerden, weil die bei dieser Transformation stört. Glücklicherweise gibt es ja Rekursion:

Code: Alles auswählen

    def zahlenraten(n):
        z = input("Rate die Zahl: ")
    	if n == z: print "Richtig!"
    	else:
    	    print "Die Zahl ist", "kleiner" if n < z else "größer"
    	    zahlenraten(n)
Nun transformieren wir:

Code: Alles auswählen

    def zahlenraten(n):
        def callback(z):
            if n == z: print "Richtig!"
        	else:
        	    print "Die Zahl ist", "kleiner" if n < z else "größer"
        	    zahlenraten(n)
        async_input("Rate die Zahl: ", callback)
Gleich viel eingängiger und übersichtlicher als das Original, oder? :) Man hat zwar das Gefühl, etwas geleistet zu haben, wenn man den folgenden JavaScript-Code geschrieben und verstanden hat, aber "blocking"-I/O in Python ist doch irgendwie einfacher:

Code: Alles auswählen

    function zahlenraten(n) {
        print("Rate die Zahl: ");
        read(function (z) {
            z = +z;
            if (z === n) print("Richtig!");
            else {
                print("Die Zahl ist " + (n < z ? "kleiner" : "größer"));
                zahlenraten(n);
            }
        });
}

Bei meinem Basic-Interpreter wollte ich jetzt nicht das gesamte Programm transformieren. Glücklicherweise ist `input` dort keine Funktion (die ich tief verschachtelt in einer Behandlung von Ausdrücken hätte verarbeiten müssen) sondern eine Anweisung. Diese verarbeite ich in einer `while`-Schleife ähnlich der, die wir schon gesehen haben:

Code: Alles auswählen

    class Basic:
        ...
        def run(self):
            while True:
                token = self.next()
                if token == "end": break
                if token == "input": self.doINPUT()
                if token == "goto": self.doGOTO()
                ...
        
        def doINPUT(self):
            name = self.next()
            self.expect(":")
            self.variables[name] = float(raw_input("? "))
        
        def doGOTO(self):
            n = self.next()
            self.expect(":")
            self.goto(n)
Würde ich die CSP-Transformation konsequent anwenden, sähe das ungefähr so aus:

Code: Alles auswählen

    class Basic:
        ...
        def run(self, cont):
            token = self.next()
            if token == "end": return cont()
            cont = bind(self.run, cont)
            if token == "input": self.doINPUT(cont)
            if token == "goto": self.doGOTO(cont)
            ...
        
        def doINPUT(self, cont):
            def callback(z):
                self.variables[name] = float(z)
                cont()
            name = self.next()
            self.expect(":")
            async_input("? ", callback)
        
        def doGOTO(self, cont):
            n = self.next()
            self.expect(":")
            self.goto(n)
            cont()
Ich brauche sie aber eigentlich nur für `input`. Alle anderen Befehle kann ich nach wie for in einer Schleife behandeln. Ich führe dazu eine Methode `step` ein, die den nächsten Befehl verarbeitet und über ihren Rückgabewert signalisiert, ob sie sofort noch einmal aufgerufen werden soll oder ob ein neuer Einstieg über einen Callback erfolgt. So sieht die neue Funktion `run` aus, die man auch "Trampoline" nennt:

Code: Alles auswählen

    class Basic:
        ...
        def run(self):
            while not self.step(): pass
        
        def step(self):
            token = self.next()
            if token == "end": return True
            if token == "input": return self.doINPUT()
            if token == "goto": return self.doGOTO()
            ...
Ich drehe die Logik so, dass `True` den Abbruch der `while`-Schleife bedeutet, weil ich auf diese Weise an allen anderen Methoden nichts ändern muss, da diese ja standardmäßig `None` (bzw. `undefined` in JavaScript) liefern, was als "falsch" gilt.

So sieht das neue `doINPUT` aus - man beachte, wie ich dort mit `self.run()` die Verarbeitung wieder starte:

Code: Alles auswählen

        def doINPUT(self):
            def callback(z):
                self.variables[name] = float(z)
                self.run()
            name = self.next()
            self.expect(":")
            async_input("? ", callback)
            return True
Die Methode `doGOTO` kann jetzt wieder so aussehen wie schon zuvor.

In JavaScript (ich habe mir erlaubt, die `do`-Methode zu einer `switch`-Anweisung zu machen) sieht das Endergebnis dann so aus:

Code: Alles auswählen

    Basic.prototype.run = function () {
        while (!this.step());
    };
    
    Basic.prototype.step = function () {
        var token = this.next();
        switch (token) {
        case 'END': return true;
        case 'INPUT':
            var name = this.next(), that = this;
            this.expect(":");
            print("? ");
            read(function (z) {
                that.variables[name] = z;
                that.run();
            });
            return true;
        case 'GOTO:
            var line = this.next();
            this.expect(":");
            this.goto(line);
            return;
        ...
        }
    };
Ich könnte eigentlich `step` auch in `run` noch einbetten und müsste noch nicht einmal meine asymmetrischen Rückgabetypen (nix oder `true`) benutzen, aber dafür wird dann die Interpreter-Schleife recht lang.

Alles in allem ist Python die bessere Sprache für Experimente mit Konsolen-Anwendungen solange es nur JavaScript-Implementierungen wie Node.js gibt, die ausschließlich asynchrones (evented) I/O anbieten.

Stefan
Zuletzt geändert von sma am Montag 28. Januar 2013, 18:06, insgesamt 1-mal geändert.
BlackJack

@sma: Ich hätte ja als erstes geschaut ob es ein synchrones `input()` schon als npm-Package gibt. Du bist sicher nicht der einzige der so etwas haben möchte. Oder sind die JavaScript-Programmierer in dem Punkt so fanatisch, dass das keiner nutzen würde? :-)
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

Ob es Fanatismus ist, weiß ich nicht. Es geht aber AFAIK technisch einfach nicht anders bei Node.js. Man kann den Callbacks nicht entrinnen. Es gibt ein asynchrones `read`-Modul, welches alles und mehr kann, was mein gezeigtes `read`-Modul beherrscht, d.h. den Teil müsste ich nicht selbst schreiben, aber etwas synchrones habe ich nicht gesehen.

Ich könnte auf `fibers` zurückgreifen, aber das ist dann das Äquivalent zu Stackless-Python , eine spezielle Node-Version, die kooperatives Multitasking erlaubt und damit auch blockierende Threads (Fibers eben) als Grundlage für blockierendes I/O kennt. Mit dem Original-Node.js geht es nicht. Ich könnte natürlich auch Rhino basierend auf der JVM benutzen, wo ich blockierendes I/O als Standard habe, doch Java hatte ich dachte ich (bis auf Android) hinter mir gelassen :)

Stefan
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

Typischer Artikel der Form: wenn ich dazu nichts gelesen hätte, hätte ich nie erfahren, dass da ein Problem existiert :D Hat mir gut gefellen! Aber eine Kleinigkeit: "seid" und nicht "seit".
Das Leben ist wie ein Tennisball.
Antworten