Python Objekt Lebensdauer (Lifetime)        (C) 2017-2022 T.Birnthaler OSTC GmbH
====================================

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

Der Begriff "Lebensdauer/Lifetime/Existenz/Gültigkeitsbereich" (wie lange ein
Python-Objekt = Wert/Objekt existiert und benutzbar ist) hat mit den Begriffen
"Scope/Sichtbarkeitsbereich" und "Namespace/Namensraum" nichts zu tun!

In Python ist ein WERT/OBJEKT existent und benutzbar ("am Leben"), solange
MINDESTENS EINE REFERENZ darauf zeigt und diese REFERENZ von Python aus direkt
per NAME oder indirekt über andere Wert/Objekte erreichbar ist.

Sobald die LETZTE REFERENZ auf ein Wert/Objekt verschwindet, ist es nicht mehr
erreichbar und sein Speicherplatz kann von Python für andere Zwecke verwendet
werden. Dazu zählt Python die ANZAHL der Referenzen auf ein Wert/Objekt
ständig in einem "Reference Counter" mit, der Teil des Wert/Objektes ist
(abfragbar per sys.getrefcount(OBJ)).

Jede zusätzliche Referenz erhöht den Referenz-Zähler um 1, jede aufgelöste
Referenz verringert den Referenz-Zähler um 1. Erreicht der Referenzzähler den
Wert 0, ist sichergestellt, dass ein Wert/Objekt nicht mehr erreichbar
("referenzierbar") und damit benutzbar ist und sein Speicherplatz wieder für
andere Aufgaben zur Verfügung steht.

  text = "hallo welt"      # 1. Referenz auf str-Objekt "hallo welt"
  ref1 = text              # 2. Referenz auf str-Objekt "hallo welt"
  ref2 = [ 1, 2, text ]    # 3. Referenz auf str-Objekt "hallo welt"

  assert id(text) == id(ref1) == id(ref2[2])   # OK

  ref2[2] = 3              # 3. Referenz auf str-Objekt zerstört
  ref1 = 123               # 2. Referenz auf str-Objekt zerstört
  del text                 # 1. Referenz auf str-Objekt zerstört

Sich gegenseitig ZYKLISCH referenzierende aber insgesamt nicht mehr
erreichbare Wert/Objekte werden von einer periodisch stattfindenden GARBAGE
COLLECTION aufgeräumt (Modul "gc"):

  import gc                # Modul "gc" importieren
  l1 = []                  # --> l1 ist leere Liste
  l2 = [l1]                # --> Liste l2 referenziert Liste l1
  l1.append(l2)            # --> Liste l1 referenziert Liste l2 --> Zyklus!
  print(l1)                # --> [[[...]]] --> Zyklus!
  print(l2)                # --> [[[...]]] --> Zyklus!
  gc.collect()             # --> ? (0 oder beliebiger anderer Wert)
  gc.collect()             # --> 0 (d.h. Speicher für 0 Objekte freigegeben)
  del l1                   # Referenz auf l1 löschen
  del l2                   # Referenz auf l2 löschen
  # --> Objekte im Zyklus l1 <-> l2 bleiben erhalten obwohl nicht erreichbar!
  gc.collect()             # --> 2 (d.h. Speicher für 2 Objekte freigegeben)
  gc.collect()             # --> 0 (d.h. Speicher für 0 Objekte freigegeben)

Am Programmende löscht der Python-Interpreter immer alle Referenzen und daher
werden auch alle Wert/Objekte aufgeräumt (d.h. ihr Speicherplatz wird
freigegeben). Dies kann durchaus zu einer WARTEZEIT zwischen dem internen und
dem externen Programmende führen.

ACHTUNG: Das Verhalten im Rahmen einer IDE (Integrated Development Environment)
ist anders, da der Python-Interpreter am Programmende von der IDE üblicherweise
NICHT automatisch beendet wird (damit man noch alle im Programm definierten
Objekte nutzen kann). In der Regel wird der Python-Interpreter in einer IDE
erst DIREKT VOR dem nächsten Programmlauf beendet.

Datenübergabe (d.h. Übergabe von Wert/Objekten) an Funktionen und Datenrückgabe
aus Funktionen erfolgt in Python grundsätzlich in Form von REFERENZEN (CALL
BY/RETURN BY REFERENCE). Diese Art der Datenübergabe ist sogar sehr EFFIZIENT.

  def f(a, b, c):          # Referenzen auf Objekte in a, b, c übergeben;
      t = (a + b, b + c)   # Neues Tupel-Objekt mit Name t erzeugt
      return t             # Referenz auf Tupel-Objekt zurückgeben

  erg = f(1, 2, 3)         # Referenzen auf Objekte 1, 2 und 3 übergeben;
                           # erg enthält zurückgegebene Referenz auf
                           # in Funktion erzeugtes Tupel-Objekt zugewiesen

PARAMETER-Variablen zeigen also auf Objekte, die von außen stammen (sind lokale
Namen, die von außen mit Objekten initialisiert werden) und stellen eine
weitere Referenz auf diese da. Sofern diese Objekte IM-MUTABLE sind, ist das
problemlos. Falls ein übergebenes Objekt MUTABLE ist, kann durch Manipulation
seines INNEREN in der Funktion ein SEITENEFFEKT (side-effect) nach außen
entstehen (den man dem Funktionaufruf nicht ansieht).

  def f(a):              # --> Lokale Variable a von außen initialisieren
      a.append("c")      # --> Inneres von lokaler Variable a manipulieren
      return len(a)      # --> Länge von lokaler Variable a zurückgeben
                         #
  lst = ["a", "b"]       # --> Globales list-Objekt erzeugen
  print(len(lst), lst)   # --> 2 ["a", "b"]
  print(f(lst))          # --> 3 (Funktion aufrufen, globales Objekt übergeben)
  print(len(lst), lst)   # --> 3 ["a", "b", "c"] (globales Objekt verändert)

In einer Funktion NEU ERZEUGTE OBJEKTE können problemlos als return-Wert
zurückgegeben werden (auch wenn der lokale Variablenname außerhalb der Funktion
außerhalb nicht mehr existiert), da schon eine einzige Referenz auf sie
außerhalb der Funktion dafür sorgt, dass sie nicht freigegeben werden können.

  def f(a, b):          #
     tpl = (a+b, a-b)   # --> Tupel-Objekt erzeugen, Name tpl ist lokal
     return tpl         # --> Tupel-Objekt zurückgeben, Name tpl verschwindet
                        #
  erg = f(1, 2)         # --> Funktion aufrufen + Rückgabe-Wert speichern
                        # --> Name "erg" zeigt auf in Funktion erzeugtes Objekt
  print(erg)            # --> (3, -1)