32. Eine erste Grammatik-Klasse

class Grammar:
  def __init__(self, sourceName):
    self._readRules(sourceName)

  def _readRules(self, sourceName):
    self.rules = {}
    for rawRule in open(sourceName).readlines():
      if rawRule.startswith("="):
        self._setStartSymbol(rawRule)
      else:
        self._addRule(Rule(rawRule))

  def _addRule(self, rule):
    self.rules.setdefault(rule.getLeft(),
      []).append(rule)

  def _setStartSymbol(self, rawSym):
    self.startSymbol = rawSym[1]

  def getRulesForNonTerm(self, nonTerm):
    return self.rules.get(nonTerm, [])

  def deriveLeftNonTerm(self, nonTerm, word):
    res = []
    for rule in self.getRulesForNonTerm(nonTerm):
      res.append(rule.applyToLeftmost(word))
    return res

  def getStartSymbol(self):
    return self.startSymbol

Anmerkungen:

  • Der Konstruktor muss hier die Grammatik aus der angegebenen Datei lesen – wir haben diese Funktionalität aber in eine separate Methode verlegt, um den Konstruktor übersichtlich zu halten (was bei komplexeren Klassen manchmal eine echte Herausforderung ist).
  • In _readRules wird eine Instanzvariable rules (korrekt mit self davor, denn sonst würde es die Variable ja nur in _readRules geben) auf das leere Dictionary initialisiert, das dann in _addRules gefüllt wird.
  • Ein paar Methoden haben Underscores vor dem Namen. Das soll andeuten, dass sie nur klassenintern verwendet und nicht von außen aufgerufen werden sollen. Python erzwingt das aber nicht. Namen mit zwei Underscores am Anfang werden von Python in der Tat “versteckt”. Wer nicht versteht, warum man das vielleicht haben möchte, soll sich vorerst keine Sorgen machen.
  • Wir haben die alte readRules-Funktion aufgespalten in die Methoden _readRules und _addRules. Das folgt der Logik, dass eine Funktion eine Sache tun soll (und die gut). In diesem Fall ist plausibel, dass das Parsen einer Regelspezifikation gemäß irgendeiner Vereinbarung unabhängig sein sollte vom Hinzufügen einer bereits maschinengemäß repräsentierten Regel zur Regelbasis der Grammatik. Im Zweifel sollte man Methoden lieber weiter aufspalten – wenn man testen will, ob zwei Aktionen in einer Methode bleiben dürfen, kann man sich überlegen, ob mindestens eine der Aktionen mit einer dritten sinnvoll kombinierbar wäre. Im vorliegenden Fall wäre es beispielsweise denkbar, dass ein zweites Grammatikobjekt einer Grammatik bereits geparste Regeln hinzufügen möchte oder wir Regeln auch in einem anderen Format verstehen wollen.
  • In dem Dictionary steht jetzt auch kein nackter String mehr, sondern Objekte einer neuen Klasse, Rule (die wir natürlich auch noch schreiben müssen).
  • Der alte Dictionary-Zugiff wird ersetzt durch eine Methode getRulesForNonTerm, was klarer macht, was der Zugriff eigentlich bedeutet.
  • Natürlich sollte auch hinter der Zeile, die die Klassendefinition einleitet, ein Docstring stehen, der grob umreißt, was die Klasse soll und was sie kann.

Inhaltlich haben noch eine Veränderung vorgenommen: In der Grammatikbeschreibung kann man jetzt nach den Regeln der Kunst auch ein Startsymbol angeben, und zwar in einer Zeile, in der nur ein = und danach das Startsymbol steht. Natürlich sollte _setStartSymbol etwas besser aufpassen, was es da eigentlich vorgesetzt kriegt und ggf. Fehler auslösen, aber das soll uns jetzt noch egal sein.

Zum Setzen der Instanzvariable startSymbol stellen wir eine Methode getStartSymbol zur Verfügung. Der Underscore vor _setStartSymbol sagt wie oben vereinbart: Klienten sollen diese Methode nicht verwenden (und in der Tat würde sie auch etwas anderes tun als ihr Name die Klienten erwarten lassen würde).

Übungen zu diesem Abschnitt

Ihr solltet euch wenigstens an den rötlich unterlegten Aufgaben versuchen

(1)

Vergleicht (soweit möglich) die Methoden unserer Grammar-Klasse mit den entsprechenden Funktionen aus gener1.py. Was hat sich geändert, was ist geblieben?


Markus Demleitner

Copyright Notice