Wir definieren noch unsere Rule-Klasse:
class Rule: def __init__(self, rawRule): self._parse(rawRule) def __str__(self): return "%s -> %s"%(self.left, self.right) def __repr__(self): return "Rule('%s')"%str(self) def _parse(self, rawRule): try: self.left, self.right = [s.strip() for s in rawRule.split("->")] if not self.left or not self.right: raise ValueError except ValueError: raise ValueError( "Syntaxfehler in %s"%rawRule) def getLeft(self): return self.left def getRight(self): return self.right def applyToLeftmost(self, word): return word.replace(self.left, self.right, 1)
Anmerkungen:
Noch etwas mehr zur Frage der Akzessorfunktionen: Ein entscheidender Vorteil der Vorschrift, Instanzvariablen nur über Akzessorfunktionen zugänglich zu machen, ist eine Entkopplung zwischen der Implementation einer Klasse (welche Instanzvariablen ich dazu brauche, ist meist eine Implementationsentscheidung) und ihrer Schnittstelle. Diese Entkopplung will man haben, weil mit ihr einzelne Bestandteile eines Programms getrennt implementiert und weiterentwickelt werden können – solange die Schnittstellen sich nicht ändern, sollte sich an der Semantik des Programms nichts ändern.
Hinzu kommt, dass sich während der Weiterentwicklung eines Programms häufig die Notwendigkeit ergibt, beim Setzen einer Instanzvariable zusätzliche Aktionen durchzuführen. Bei unserem Toy-Beispiel könnte durch das Setzen einer Instanzvariable hunger indirekt der Wert einer anderen Instanzvariable mood beeinflusst werden. In einer Akzessorfunktion können wir dafür sorgen, dass der Zustand des Objekts konsistent bleibt, indem wir ggf. mood mitaktualisieren. Hätten wir den direkten Schreibzugriff auf hunger erlaubt, könnte man von außen inkonsistente Zustände erzeugen.
Auch der Lesezugriff ist nicht unkritisch, vor allem, wenn sich die Implementation ändert. So könnte es beispielsweise sein, dass wir hunger irgendwann nicht mehr direkt speichern, sondern aus, beispielsweise, stomachFill und glucoseLevel berechnen. Eine getHunger-Methode kann diese innere Änderung nach außen hin maskieren und dafür sorgen, dass die Klienten ungeändert weiter funktionieren, ein direkter lesender Zugriff auf hunger würde dagegen scheitern (alternativ müssten wir eben immer die Instanzvariable hunger mitführen, obwohl wir sie eigentlich aus anderen Parametern berechnen können, und auch das wäre nicht schön.
(Weiterführend:) Die mit Python 2.2 eingeführten Deskriptoren erlauben eine gewisse Kontrolle über das, was mit existierenden Einträgen in die Namensräume passieren kann – mit Deskriptoren und new-style classes könnte man also direkten Zugriff auf Attribute zulassen, ohne sich über die zukünftige Entwicklung Sorgen machen zu müssen. Ich werde in diesem Skript allerdings fast nichts zu diesen Themen sagen, und vorläufig sind sie so esoterisch, dass ihr euch nicht darum kümmern wollt.
Der Code für Grammar und Rule möge in grammarsimple.py stehen. Dann:
>>> import grammarsimple >>> r = grammarsimple.Rule("A->BC") >>> r Rule('A -> BC') >>> print r A -> BC >>> r.applyToLeftmost("aaA") 'aaBC'
Im gener-Programm muss sich an generate nichts ändern (obwohl es natürlich nett wäre, rules jetzt etwa grammar zu nennen), deriveLeft wird einfacher, weil die Arbeit, die von Grammatik oder Regeln gemacht werden sollte, auch dort gemacht wird:
def deriveLeft(curWord, grammar): leftmostNonTerm = getLeftmostNonTerm(curWord) return grammar.deriveLeftNonTerm( leftmostNonTerm, curWord)
Das “Hauptprogramm” könnte dann sein:
if __name__=="__main__": gram = Grammar("grammar") generate(gram.getStartSymbol(), gram, 5)
Ihr solltet euch wenigstens an den rötlich unterlegten Aufgaben versuchen
(1)
Sammelt alle nötigen Bausteine für einen kompletten Generierer auf der Basis unserer neuen Klassen aus den hier vorgestellten Bausteinen und den alten Funktionen zusammen (wohl am besten im grammarsimple.py-Modul). Kommentiert die einzelnen Klassen und Funktionen (für Doctests gibts extra Lob). Überzeugt euch, dass das Programm immer noch das tut, was es soll, etwa durch Vergleich der Ausgaben von gener1 und dem neuen Programm (hochtrabend könnte man das einen “Regressionstest” nennen).
Vergesst beim Testen nicht, dass die Grammatikdateien jetzt um das Startsymbol (also in der Regel eine Zeile “=S”) ergänzt werden müssen, sonst wird euer Programm, wie es jetzt ist, etwas wie “AttributeError: Grammar instance has no attribute startSymbol” sagen (was zugegebenermaßen für Leute, die die Grammatiken schreiben, keine hilfreiche Fehlermeldung ist – wie ließe sich das am einfachsten verbessern?)
(2)
Der Umstand, dass Namespaces von Objekten beliebig schreibbar ist, kann manchmal auch praktisch sein. Folgendes könnte z.B. bestimmte Anwendungen von Dictionaries abdecken:
>>> class Empty: ... pass ... >>> e = Empty() >>> e.bla, e.rotz = "eins", "zwei" >>> e.bla 'eins'
Denkt euch aus, wie ihr rauskriegt, ob diese Sorte von “emuliertem” Dictionary oder Dictionaries selbst schneller sind. Überlegt euch, unter welchen Umständen man sowas vielleicht machen wollen könnte.