49. Sichtbarkeit und Blöcke

In der letzten Fassung von generate haben wir die globale Variable allDerivations verwendet. Das ist unschön und sollte natürlich so nicht gemacht werden – globale Variablen hatten wir schon vor geraumer Zeit als gefährlich deklariert. Wir hatten damals aber auch gesagt, dass Zuweisungen immer in den lokalen Namespace gehen. Dass allDerivations hier tatsächlich alle Ableitungen speichert, liegt daran, dass wir nicht zuweisen, sondern einen bestehenden Wert (der im globalen Namespace gefunden wird) per append ändern.

In Wirklichkeit wäre es an dieser Stelle günstiger, allDerivations als weiteres Argument zu übergeben oder (besser) die jeweilig erzeugten Ableitungen zurückzugeben und “auf dem Rückweg” aus der Rekursion zu sammeln.

An dieser Stelle müssen wir nochmal zur Frage von lokalen und globalen Variablen zurückkehren.

Python hält drei Dictionaries für Namen: Einen für lokale, einen für globale und einen für eingebaute (wie open, map). Die Zuweisung a = 3 bedeutet letztlich etwas wie someDict["a"] = 3. someDict ist das lokale Dictionary, wenn wir in einer Funktion (oder Klassendefinition) sind, das globale sonst.

Wenn wir auf eine Variable var zugreifen, passiert in etwa das:

try:
  return localDict["var"]
except KeyError:
  try:
    return globalDict["var"]
  except KeyError:
    return builtinDict["var"]

Deshalb sieht man globale Variablen, während man bei Zuweisungen ins lokale Verzeichnis schreibt.

Die eingebauten Funktionen locals() und globals() geben diese Dictionaries zurück. Wer sie verändert, bekommt Ärger.

Globale Variablen sind gefährlich!

In ganz seltenen Fällen kann es einmal vorkommen, dass man doch aus einer Funktion heraus das globale Dictionary ändern möchte. Auch in diesen Fällen darf man nicht über globals() gehen, sondern kann das Schlüsselwort global verwenden:

>>> globVar = "ich"     >>> def checkGlob():
>>> def checkGlob():    ...     global globVar
...     globVar = "du"  ...     globVar = "du"
...     print globVar   ...     print globVar
...                     ...
>>> checkGlob()         >>> checkGlob()
du                      du
>>> globVar             >>> globVar
'ich'                   'du'

Aber wie gesagt: Ihr könnt ein Leben lang programmieren, ohne je in eine Sitution zu kommen, in der sowas eine gute Lösung wäre. Vergesst es am besten gleich wieder.

Die Regel, welche lokalen Dictionaries für welchen Code zuständig sind, hängt eng mit dem Begriff des Blocks zusammen. Ein Block ist ein Stück Code, das als Einheit ausgeführt wird – genauer sind Python Module, Skripte, Funktionskörper und Klassendefinitionen Blöcke, und zusätzlich noch ein paar Exoten wie die Strings, die man an eval oder exec übergibt. Jeder Block hat sein eigenes lokales Dictionary (im Falle von Modulen und Skripten ist das lokale gleich dem globalen Dictionary).

Es ist nützlich, sich klar zu machen, dass Blöcke (in dieser Sprechweise) von compound statements wesentlich verschieden sind, da in der Literatur die beiden Begriffe nicht immer streng getrennt werden. In Sprachen wie C, Java oder Pascal haben compound statements in verschiedenem Maße Eigenschaften von Blöcken – sie können beispielsweise selbst lokale Variablen enthalten, die außerhalb des compound statements nicht sichtbar sind. In Python ist das nicht so; compound statements sind eine rein syntaktische Geschichte, um zusammengehörige Anweisungen zu gruppieren, während Blöcke über die Sichtbarkeit von Variablen entscheiden.

Lexikalisches Scoping

In Wirklichkeit ist es noch komplizierter: Steht ein Block lexikalisch (also sozusagen “im Quelltext”) innerhalb eines anderen, so wird (seit Python 2.3, in 2.2 kann man das Verhalten durch from __future__ import nested_scopes bestellen) auch in den umschließenden Namespaces gesucht – das heißt lexikalisches Scoping:

>>> def makeAppender(aList):
...   return lambda item: aList.append(item)
...
>>> l = []
>>> app = makeAppender(l)
>>> app(1);app("zwei")
>>> l
[1, 'zwei']

Um hinter das lexikalische Scoping zu kommen, müsst ihr die Definition des lambda ansehen: In dessen Funktionskörper wird der Name aList verwendet, der darin eigentlich gar nicht definiert ist. Er wird zur Laufzeit von makeAppender ausgefüllt, und zwar mit dem Wert von aList im Namespace von makeAppender, ohne dass aList jemals im Namespace des lambda (oder eben dem globalen) gewesen wäre.

Auch wenn das jetzt nicht so nützlich aussieht: Funktionen, in die man auf diese Weise Kontext “einfriert” sind sehr nützlich, wenn man gegenseitige Abhängigkeiten reduzieren und Schnittstellen vereinfachen möchte. Wir haben Ähnliches bisher mit Default-Argumenten gemacht, aber da diese immer noch überschrieben werden können, ist der Umweg über lexikalisches Scoping eleganter.

Im Fachjargon heißen Funktionen mit eingefrorenem Kontext closures.

Übungen zu diesem Abschnitt

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

(1)

Probiert das Beispiel mit der checkGlob-Funktion aus. Wenn ihr nicht ganz sicher seid, was da vorgeht, lasst euch die globals() und locals() zwischen den Zeilen ausgeben und beobachtet, wie sie sich verhalten.

(2)

Lasst euch im Interpreter locals() und globals() ausgeben. Dann weist einer Variable einen Wert zu und seht euch nochmal die beiden Dictionaries an. Was beobachtet ihr? Gibts dafür eine Erklärung? Was passiert, wenn ihr das innerhalb einer Funktion macht?

(3)

Mit der eingebauten Funktion dir könnt ihr euch ausgeben lassen, was so alles in einem Namespace drin ist. Euch ist in der Ausgabe von globals() vermutlich der komische Eintrag für __builtins__ aufgefallen. Benutzt dir, um nachzusehen, was da so alles drinsteht. Und lasst euch nicht dabei erwischen, wie ihr da drin rumpfuscht


Markus Demleitner

Copyright Notice