44. Default-Argumente

Unser Programm könnte jetzt so aussehen:

import sys
import grammar, word, symbol

def deriveLeft(curWord, grammar):
  leftmostNonTerm = curWord.getLeftmostNonTerm()
  return grammar.deriveLeft(leftmostNonTerm,
    curWord)

def generate(curWord, grammar, maxLen,
  seenWords={}):
  if seenWords.has_key(curWord):
    return
  else:
    seenWords[curWord] = 1
  if len(curWord)>maxLen:
    return
  if curWord.isAllTerminal():
    print curWord
    return
  for deriv in deriveLeft(curWord, grammar):
    generate(deriv, grammar, maxLen, seenWords)

if __name__=="__main__":
  grammar = Grammar.Grammar(sys.argv[1])
  generate(word.Word(str(
    grammar.getStartSymbol())),
    grammar, int(sys.argv[2]))

Anmerkungen:

  • Unsere generate-Funktion hat sich bemerkenswert wenig verändert – wir haben sie also von vorneherein ganz gut entworfen.
  • Die wesentlichste Änderung ist, dass wir jetzt noch ein Dictionary seenWords mitführen, in das wir die wir die Wörter eintragen, die wir schon behandelt haben. Damit verhindern wir, dass Wörter mehrfach erzeugt werden und sparen unter Umständen erheblich Rechenzeit ein. Die Verwendung von Dictionaries als Mengen (etwas ist drin oder nicht) ist ziemlich üblich. Es wäre zwar auch denkbar, für so etwas Listen zu verwenden, Dictionaries haben aber die Vorteile, dass (1) von Natur aus jeder Schlüssel nur vorkommt oder nicht vorkommt, während bei Listen ohne weiteres jedes Element beliebig oft vorkommen könne (was bei Mengen unerwünscht ist) und (2) sehr schnell geprüft werden kann, ob ein Schlüssel vorhanden ist, ein Element also in der Menge ist. Unsortierte Listen müssen für so eine Prüfung immer komplett durchsucht werden.
  • Ab Python 2.3 gibt es auch eigene Mengentypen, so dass Mengen nicht mehr so emuliert werden müssen. Die Verwendung von Dictionaries wird uns allerdings später ermöglichen, mit einer kleinen Codeänderung auch Ableitungen verfolgen zu können.

Generate verwendet ein Default-Argument, um das seenWords-Dictionary beim Aufruf ohne Argumente (also dem nichtrekursiven Aufruf) korrekt zu initialisieren.

Default-Argumente dienen häufig zur Festlegung eines Standardfalls, etwa bei open: Normalerweise wird lesend geöffnet, wenn aber ein zweites Argument vorhanden ist, wird er zum Modus. Anderes Beispiel:

>>> def inc(var, amount=1):
...     return var+amount
...
>>> inc(3)
4
>>> inc(3,8)
11

Damit ist auch das eigenartige yieldRules-Argument aus der Grammatik-Klasse klar: Normal (d.h., wenn wir yieldRules nicht geben) gibt grammar.deriveLeft nur die Ergebnisse der Anwendung von Regeln zurück. Übergibt man aber ein „wahres” yieldRules, kommt eine Liste von Tupeln aus angewandter Regel und resultierendem Wort zurück.

Man kann Default-Argumente auch als keyword parameters verwenden:

>>> def incTuple(tup, am1=1, am2=1):
...     return (tup[0]+am1, tup[1]+am2)
...
>>> incTuple((1,1), am2=3)
(2, 4)

Die Regeln für die Zuweisung von Aktualparametern (also dem, was im Funktionsaufruf steht) zu Formalparametern (also dem, was im Funktionskopf steht) sind etwa so:

  1. Belege zuerst jeden „freien” Formalparameter mit dem Aktualparameter mit gleichem Index
  2. Belege dann jeden Default-Parameter mit dem im Funktionskopf angegebenen Wert
  3. Solange noch „freie” Aktualparameter übrig sind, belege die Default-Parameter nach Reihenfolge, als wären sie von vorneherein frei gewesen
  4. Weise die Werte aus den Keyword-Parametern den Formalparametern mit den passenden Namen zu
  5. Sollte noch ein „freier” Aktualparameter nach dem ersten Keyword-Parameter kommen, löse einen Syntaxfehler aus

Die wirklichen Regeln sind übrigens noch etwas komplizierter – im Zweifel gibt die Python Language Reference erschöpfende Auskunft. Aus diesen Regeln folgt, dass auch ohne Default-Parameter Keyword-Parameter verwendet werden können, um etwa ganz deutlich zu machen, welche Parameter welche Werte bekommen. Bei langen Argumentlisten kann sowas helfen, blöde Fehler zu vermeiden.

>>> def foo(bar, baz, bong):
...     print bar, baz, bong
...
>>> foo(bong=4, baz="baz", bar=9)
9 baz 4

Eine weitere Anwendung für Default-Argumente ist die Vermeidung von globalen Variablen bei Funktionen. In einer Hausaufgabe haben wir die Fibonacci-Zahlen rekursiv berechnet und wollten uns bereits berechnete Werte merken. Damals konnten wir das nur mit einer globalen Variable, was nicht gut ist. Die folgende Fassung vermeidet das:

def fib(n, valueCache={}):
  if valueCache.has_key(n):
    return valueCache[n]
  if n==1 or n==0:
    return 1
  else:
    valueCache[n] = fib(n-1)+fib(n-2)
    return valueCache[n]

Das funktioniert, weil der Wert des Default-Arguments dann gesetzt wird, wenn die Funktion „compiliert” wird, also zu dem Zeitpunkt, zu dem der Interpreter die Zeile mit dem def verarbeitet. Bei jedem Aufruf von fib mit nur einem Argument hat danach valueCache im das gleiche Dictionary als Wert, eben das, in das wir all die Werte reingeschrieben haben. Dieser Trick wird uns später nochmal im Zusammenhang mit so genannten „Closures” begegnen.

Noch eine kurze Anmerkung zur Terminologie: Ich versuche, in diesem Skript meistens von Argumenten zu sprechen, wenn ich „Dinge, die an Funktionen übergeben werden” meine. Das Wort Parameter ist dazu weitgehend synonym (auch wenn manche Autoren da Unterschiede machen). Im Fall von Formal- und Aktualparametern ist allerdings die Verwendung des Wortes Argument sehr unüblich, weshalb ich hier doch Parameter sage. Der Grund dieser „selectional preference” ist wohl, dass einige einflussreiche Autoren versucht haben, Parameter als sozusagen syntaktische Kategorie zu fassen, während sie das Wort Argument für den konkreten Wert eines Parameters reservieren wollten. Letztlich ist das aber alles egal, die Leute reden in diesem Bereich, wie es ihnen gerade passt.

Übungen zu diesem Abschnitt

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

(1)

Nehmt an, ihr hättet ein großes Programm, das jede Menge Fehlermeldungen produzieren kann. Da nicht von vorneherein klar ist, wie und in welcher Sprache die Fehlermeldungen ausgegeben werden, soll eine eigene Funktion die Ausgabe der Fehler übernehmen. Dabei ist jedem Fehler eine Zahl zugeordnet (die dann wiederum besser in Klartextkonstanten definiert sein sollte: fileNotFoundError = 7 o.dgl.). Schreibt also eine Funktion reportError(errCode=-1, errTable=englishTable, fatal=0) -> None, die die Fehlermeldung zum errCode aus der Liste errTable nach stderr ausgibt und das Programm abbricht, wenn fatal wahr ist. Ist errCode==-1, soll das Programm etwas wie „No error specified” ausgeben.

(2)

Schreibt eine Klasse Set, die ein paar Mengenoperationen bereitstellt: Man soll Elemente hinzufügen und wegnehmen sowie auf Mitgliedschaft testen können, darüber hinaus wäre vielleicht noch die Berechnung noch Schnitt und Vereinigung zweier Mengen nett. Intern sollte die Menge wohl trotzdem als Dictionary dargestellt werden.


Markus Demleitner

Copyright Notice