33. Eine Regel-Klasse

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:

  • Wir haben zwei weitere spezielle Namen neben init: __str__ soll einen String zurückgeben, der für den Menschen schön anzusehen ist (wird z.B. bei print verwendet oder auch von der str-Funktion), __repr__(für Repräsentation), soll dagegen tiefere Einblicke in das Objekt liefern, wenn möglich sogar Python-Code, der zum Neu-Erzeugen des Objekts verwendet werden könnte.
  • Für die Instanzvariablen, die wir zugänglich machen wollen, haben wir Zugriffs- oder Akzessorfunktionen (getRight und Freunde). Das ist empfohlene Praxis, auch wenn in Python niemand gehindert wird, trotzdem auch lesend oder schreibend auf Attribute von Objekten zuzugreifen oder gar von außen neue Attribute anzulegen: Python-Objekte haben, im Gegensatz zu Sprachen etwa aus der Familie von Java und C++, keinerlei besondere Kontrolle über ihre Namensräume. Das kann manchmal angenehm sein, führt aber bei Missbrauch meistens zu chaotischem und unwartbarem Code. Daher: Wenn ihr wollt, dass eine Instanzvariable von außen lesbar ist, schreibt eine Methode get<varname>, wollt ihr, dass sie schreibbar ist, schreibt eine Methode set<varname>. Und schreibt vor, dass von außen nie direkter Zugriff auf Instanzvariablen erlaubt ist. Macht es auch bei Objekten anderer Leute nicht, es sei denn, der/die AutorIn der Klasse würde euch explizit dazu auffordern.
  • Wieder ist die Klasse so geschrieben, dass der Konstruktor “rohe” Daten bekommt, sie veredelt und das Produkt dem Objekt zur Verfügung stellt.
  • Die Methode applyToLeftmost abstrahiert die Anwendung einer Regel – eine der größten Vorteile des objektorientierten Programmierens ist, dass Verhalten explizit gemacht werden kann. Eine Regel kann jetzt einfach “angewendet” werden, wir sagen also konkret, was die Wirkung einer Aktion in der Anwendung ist. Ganz egal, wie wir eine Regel repräsentieren, in jedem Fall muss sie angewendet werden können.

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)

Übungen zu diesem Abschnitt

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.


Markus Demleitner

Copyright Notice