Python Scopes (Sichtbarkeitsbereich)       (C) 2017-2024 T.Birnthaler OSTC GmbH
====================================

--> python-scope.txt        Scope/Sichtbarkeitsbereich
--> python-namespace.txt    Namespace/Namensraum
--> python-lifetime.txt     Lebensdauer/Lifetime/Existenz/Gültigkeitsbereich

Um auf den hinter einem NAMEN steckenden WERT/OBJEKT zuzugreifen, werden gemäß
folgender "LEGB-Regel" alle Scopes (Sichtbarkeitsbereiche) in einem Programm in
folgender REIHENFOLGE von oben nach unten (d.h. von "innen nach außen") nach
diesem Namen durchsucht ("NAME-LOOKUP"). Ein SCOPE ist eine NAMENS-TABELLE, zu
jedem Namen ist darin eine REFERENZ auf den aktuell ihm zugeordneten
WERT/OBJEKT gespeichert. Beim ersten Treffer ist die Suche zu Ende, ist der
Name in keinem Scope vorhanden, bricht Python das Programm mit einem
"NameError"-Fehler ab.

 +------------+---+------------------------------------------------+  ||
 | L)ocal     | 1 | Funktion (auch lambda, Comprehension)          |  || innen
 | E)nclosing | 2 | Einschachtelnde Funktion (z.B. Dekorator)      |  ||   :
 | G)lobal    | 3 | Hauptprogramm (Skript, Modul)                  |  ||   :
 | B)uilt-in  | 4 | Python-Interpreter (z.B. len, sum, print, ...) |  || außen
 +------------+---+------------------------------------------------+  \/

ACHTUNG: In Python gibt es KEINEN BLOCK-SCOPE, d.h. keine block-lokalen Namen.
BLÖCKE von zusammengehörenden Anweisungen (= GROSSE Anweisung) werden in Python
durch gleichförmiges Einrücken nach einem ":" gebildet (in vielen anderen
Sprachen durch Einschachteln in geschweifte Klammern {...}).

ACHTUNG: Auch LAUFVARIABLE von for-Schleifen sind nicht lokal zur Schleife (und
enthalten daher nach der Schleife den letzten in der Schleife erreichten Wert):

  var = 0                   # var ist globale Variable (i, j unbekannt)
  for i in range(1,10+1):   # i ist globale Variable   (j unbekannt
      j = 10                # j ist globale Variable
      var += i              # var, i sind globale Variable
  print(var, i, j)          # var, i, j sind globale Variable

Folgendes Code-Stück zeigt das LEGB-Verhalten beim "Name-Lookup"
(Spalte "R" = Ausgabe-Reihenfolge)

 +------------------------+--------------------+---+-------------------------+
 | Code                   | Aktion             | R | Ausgabe                 |
 +------------------------+--------------------+---+-------------------------+
 | print(len)             |                    | 1 | <built-in function len> |
 | def f_aussen():        | Def. Funktion      |   |                         |
 |     len = "E)nclosing" | Def. lok. Var.     |   |                         |
 |     print(len)         |                    | 3 | E)nclosing              |
 |     def f_innen():     | Def. Funktion      |   |                         |
 |         len = "L)okal" | Def. lok. Var.     |   |                         |
 |         print(len)     |                    | 5 | L)ocal                  |
 |     print(len)         |                    | 4 | E)nclosing              |
 |     f_innen()          | Aufruf "f_innen"   |   |                         |
 |     print(len)         |                    | 6 | E)nclosing              |
 | print(len)             |                    | 2 | <built-in function len> |
 | f_aussen()             | Aufruf "f_aussen"  |   |                         |
 | print(len)             |                    | 7 | <built-in function len> |
 | len = "G)lobal"        | Def. glob. Var.    |   |                         |
 | print(len)             |                    | 8 | G)lobal                 |
 | del len                | Löschen glob. Var. |   |                         |
 | print(len)             |                    | 9 | <built-in function len> |
 +------------------------+--------------------+---+-------------------------+

Auf eine GLOBALE Variable kann in Funktionen LESEND zugegriffen werden,
falls keine gleichnamige LOKALE Variable durch Zuweisung gebildet wird:

  gv = "global"   # Globale Variable gv erzeugen
                  #
  def f(...):     # Funktion f() definieren
      print(gv)   # Globale Variable gv lesen (OK)
                  #
  def g(...):     # Funktion g() definieren
      gv = 123    # Lokale Variable gv erzeugen (OK, gleichnamig)
      print(gv)   # Lokale Variable gv lesen    (OK)

Erst eine globale Variable LESEN und dann eine gleichnamige lokale Variable
ERZEUGEN ist nicht erlaubt (löst den Fehler "UnboundLocalError" mit der Meldung
"local variable 'gv' referenced before assignment" aus):

  def g(...):     # Funktion g() definieren
      print(gv)   # Globale Variable gv lesen   (OK)
      gv = 123    # Lokale Variable gv erzeugen (PENG, gleichnamig)

Per Schlüsselwort "global" kann eine Funktion auf einen GLOBALEN Namen
des Hauptprogramms LESEND + SCHREIBEND zugreifen (diese "Schweinerei" wird
also durch das Schlüsselwort "global" deutlich sichtbar gemacht):

  gv = "global"       # Globale Variable gv erzeugen
                      #
  def f(...):         # Funktion f() definieren
      global gv       # Globale Variable gv lesen + schreiben erlauben
      print(gv)       # Globale Variable gv lesen     (OK)
      gv = "lokal"    # Globale Variable gv schreiben (OK)
      print(gv)       # Globale Variable gv lesen     (OK)

Per Schlüsselwort "nonlocal" kann eine Funktion auf einen LOKALEN Namen der sie
direkt umgebenden (enclosing) "Wrapper"-Funktion zugreifen (LESEN + SCHREIBEN).

  def f(...):             # Funktion f() definieren
      lv = "enclosing"    # Lokale Variable lv in f() erzeugen
                          #
      def g(...):         # Eingeschachtelte (lokale) Funktion g() definieren
          nonlocal lv     # Nichtlokale Variable lv von f() verwenden
          print(lv)       # Nichtlokale Variable lv von f() lesen     (OK)
          lv = "local"    # Nichtlokale Variable lv von f() schreiben (OK)
          print(lv)       # Nichtlokale Variable lv von f() lesen     (OK)

Auch mehrfach bei mehreren ineinander verschachtelten Funktionen möglich:

  def f(...):                 # Funktion f() definieren
      lv = "enclosing2"       # Lokale Variable lv in f() erzeugen
                              #
      def g(...):             # Eingeschachtelte (lokale) Funktion g() definieren
          nonlocal lv         # Nichtlokale Variable lv von f() verwenden
          print(lv)           # Nichtlokale Variable lv von f() lesen     (OK)
          lv = "enclosing1"   # Nichtlokale Variable lv von f() schreiben (OK)
          print(lv)           # Nichtlokale Variable lv von f() lesen     (OK)
                              #
          def h(...):         # Eingeschachtelte (lokale) Funktion h() definieren
              nonlocal lv     # Nichtlokale Variable lv von f() verwenden
              print(lv)       # Nichtlokale Variable lv von f() lesen     (OK)
              lv = "local"    # Nichtlokale Variable lv von f() schreiben (OK)
              print(lv)       # Nichtlokale Variable lv von f() lesen     (OK)

GEBUNDENE NAMEN in COMPREHENSIONS (die also von der Comprehension mit einem
Wert gefüllt werden), haben LOCAL SCOPE, d.h. außerhalb der Comprehension sind
sie nicht sichtbar:

    qz = [(a, b, c) for a in range(1,11)       # a, b, c lokal zur Comprehension
                    for b in range(1,11)
                    for c in range(1,11)
                    if a < b < c
                    if a**2 + b**2 == c**2]

    text = "hallo welt wie geht es dir"
    freq = { z : text.count(z) for z in set(text) }   # z lokal zur Comprehension

Ebenso haben GEBUNDENE NAMEN in LAMBDA-FUNKTIONEN (die also vom Aufruf der
lambda-Funktion mit einem Wert gefüllt werden) LOCAL SCOPE, d.h. außerhalb der
lambda-Funktion sind sie ebenfalls nicht sichtbar:

    erg = sorted(..., key=lambda x: abs(x))   # x lokal zu lambda-Funktion