Seid froh, dass Python eine blockierende input-Funktion hat
Verfasst: Sonntag 27. Januar 2013, 18:10
[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:
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:
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:
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:
Nun transformieren wir:
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:
}
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:
Würde ich die CSP-Transformation konsequent anwenden, sähe das ungefähr so aus:
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:
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:
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:
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
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"
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))

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;
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)
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)

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)
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()
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()
...
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
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;
...
}
};
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