Problem bei der Freigabe über __del__

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
Achtzig
User
Beiträge: 5
Registriert: Freitag 11. Mai 2007, 10:41

Freitag 1. Juni 2007, 10:13

Hallo und erstmal vielen Dank, für die vielen Antworten, die ich schon in diesem Forum gefunden habe.

Als Python-Neuling (aber mit über 20jähriger Erfahrung in Assembler und Pascal) staune ich über folgende Gegebenheit:

Code: Alles auswählen

# ModA.py

class ClsA:
  def __init__(self)
    pass
  def __del__(self)
    print 'DelA'


# ModB.py

from ModA import ClsA

class ClsB (ClsA):
  def __init__(self)
    ClsA.__init__(self)
  def __del__(self)
    print 'DelB'
    ClsA.__del__(self)

abc=ClsB()
Der Aufruf von ModB gibt erwartungsgemäß DelB DelA aus. Wenn ich nun abcdef=ClsB() eintrage, kommt der Fehler ...None Type object has no attribute __del__ von ClsB. ClsA befindet sich also bei der Freigabe nicht mehr im Namensraum. Kann mir aber nicht vorstellen, daß die Länge des Bezeichners ausschlaggebend ist, da ich im Zusammenhang mit __del__ auch andere Merkwürdigkeiten festgestellt habe (beispielsweise eine Instanz, die nur eine Objektvariable vom Typ int enthält und bei einem negativen Wert funktioniert und bei einem positiven Wert die obengenannte Fehlermeldung ausgibt, obwohl der Wert nur in __init__ zugewiesen wird und ansonsten keine Beachtung findet - zu Testzwecken natürlich).

Ich hoffe, Ihr könnt Euch in etwa vorstellen, was ich meine. Mich würde es nähmlich nicht wundern, wenn Ihr den Fehler nicht reproduzieren könnt. Als Ausweg benutze ich gerade die folgende Variante, die ich aber nicht sehr sauber finde:

Code: Alles auswählen

class ClsB (ClsA):
  ...
  def __del__(self)
    from ModA import ClsA
    print 'DelB'
    ClsA.__del__(self)
Ich nutze Python 2.5.1 mit der IDE SPE 0.8.3.c und würde mich freuen, wenn jemand eine Idee zur Problemlösung hätte.

Mit freundlichen Grüßen
Achtzig
Leonidas
Administrator
Beiträge: 16024
Registriert: Freitag 20. Juni 2003, 16:30
Kontaktdaten:

Freitag 1. Juni 2007, 10:39

Hallo Achtzig, willkommen im Forum,

``__del__`` ist zwar der Destruktor, aber er funktioniert ganz anders als man sich das Denken würde. Man kann nich direkt Vorhersagen wann er aufgerufen wird und außerdem behindert er die Garbage Collection. Daher ist mein Tipp, ``__del__`` soweit es möglich ist, zu meiden. SIehe auch die Warnungen in der Dokumentation. Wenn du wirklich sowas wie einen Destruktor brauchst dann schau dir mal die Rezepte #519621, #519610 und #519635 an.
My god, it's full of CARs! | Leonidasvoice vs Modvoice
BlackJack

Freitag 1. Juni 2007, 10:42

Erster Vorschlag: Vergiss `__del__()`. Oder lies Dir zumindest die Dokumentation durch und verstehe was die Methode alles *nicht* garantiert, insbesondere ob und wann sie aufgerufen wird und welche Objekte zu dem Zeitpunkt noch erreichbar sind. Was Du da siehst ist kein Bug, sondern dokumentiertes Verhalten.

Das Problem besteht in Deinem Fall darin, dass bei Programmende die Objekte, auf die es keine Referenzen mehr gibt, in mehr oder weniger beliebiger Reihenfolge "beseitigt" werden.

Das Ändern eines Namens kann die Reihenfolge der Objekte im Dictionary des Modulnamensraumes führen und damit kann es dann sein, dass durch Ändern des Namens das Programm funktioniert oder nicht, ganz einfach weil sich die Reihenfolge des "Abräumens" dadurch ändern kann.

Sicher benutzen kann man in `__del__()` nur Objekte, die an die Instanz oder Klasse gebunden sind, weil darauf auch garantiert von dem Objekt noch Wege über Referenzen bestehen. Selbst ein einfaches ``print 'Hallo'`` könnte einem theoretisch um die Ohren fliegen, wenn `sys.stdout` bereits nicht mehr existiert.

Der ``import`` ist nicht unbedingt notwendig, da es einen "Pfad" von der Instanz über die Klasse zur Oberklasse gibt, dem man folgen kann:

Code: Alles auswählen

In [7]: class A(object): pass
   ...:

In [8]: class B(A): pass
   ...:

In [9]: b = B()

In [10]: b.__class__.__base__
Out[10]: <class '__main__.A'>
Aber wie gesagt: Vergiss `__del__()`…
Achtzig
User
Beiträge: 5
Registriert: Freitag 11. Mai 2007, 10:41

Freitag 1. Juni 2007, 10:48

funktioniert ganz anders als man sich das Denken würde
Danke, daß erklärt einiges. Bei Pascal funktioniert das mit den Konstruktoren und Destuktoren ganz anders. Werde Deine Verweise nachher durchlesen und bin dann hoffentlich schlauer als jetzt :lol:

Mit freundlichen Grüßen
Achtzig

Edit: @BlackJack danke für die Antwort. mehr oder weniger beliebiger Reihenfolge macht mich gerade etwas nervös, werde mich aber daran gewöhnen :D
lunar

Freitag 1. Juni 2007, 18:13

Achtzig hat geschrieben:
funktioniert ganz anders als man sich das Denken würde
Bei Pascal funktioniert das mit den Konstruktoren und Destuktoren ganz anders.
Welcome to the mysteries of garbage collection ;)

Kein Wunder, dass die Dinge in Pascal anders funktioniert, diese Sprache verlangt ja noch das händische Aufräumen mittels expliziter Destruktoraufrufe. Bei anderen Sprachen, welche Garbage Collection bieten, unterliegen Destruktoren übrigens ähnlichen Einschränkungen. Sowohl Java als auch C# garantieren z.B. weder einen bestimmten Aufrufzeitpunkt noch einen besondere Reihenfolge bei der Ausführung von Destruktoren.
Achtzig
User
Beiträge: 5
Registriert: Freitag 11. Mai 2007, 10:41

Samstag 2. Juni 2007, 08:54

Andersrum gesehen könnte die Basisklasse aber dennoch solange bereitgehalten werden, bis eine abgeleitete Klasse aufgelöst ist. Wenn ein Objekt nicht mehr referenziert wird und der beanspruchte Speicherbereich irgendwann durch garbage collection tatsächlich freigegeben wird, ist es mir egal, zu welchen Zeitpunkt das passiert. Wichtig ist, daß die ganze Destruktorenkette durchlaufen werden kann. Ein Objekt wird ja auch nicht feigegeben, solange es referenziert wird. Mir fehlt da ein bischen die dreidimensionale Auflösung, die ja einen wesendlichen Aspekt in der objektorientierten Programmierung darstellt.

Garbage collection an sich ist für mich nichts Neues, da es in den Achtzigern schon in BASIC-Dialekten verwendet wurde. Ungewöhnlich daran ist nur, daß eben auch Bezeichner aus dem Namensraum verschinden, die noch von einer Unterklasse benötigt werden. Das halte ich für ein Manko. Für mich ist der kontrollierte Abbau genauso wichtig wie der kontrollierte Aufbau (und der Rest dazwischen). Der Destruktor von object könnte beispielsweise ein Signal setzen, daß das Objekt nun (irgendwann) aus dem Speicher entfernt werden kann. Damit wäre gewährleistet, daß alle abgeleiteten Destruktoren durchlaufen wurden.

Nichtsdestsotrotz gefällt mir Python sehr gut. Die ganzen Semikolons und begin end und Blockklammern von anderen Programmiersprachen haben mich schon immer genervt. Python macht da einen sehr aufgeräumten Eindruck und bis auf die Destruktoren läuft ja auch alles, wie ich es erwarte oder nachvollziehen kann.

Das Fazit ist jedenfalls, daß ich nun vom Hauptprogramm aus den geregelten Abbau vornehme, also alle Objekte, bei denen der Zeitpunkt der Freigabe nicht zufällig sein sollte, über eine Liste referenziere und die Referenzen in umgekehrter Reihenfolge freigebe. Das gleiche dann auch bei den Gruppenobjekten, die ihrerseits Objektreferenzen enthalten.

Mit freundlichen Grüßen
Achtzig
lunar

Samstag 2. Juni 2007, 09:04

Achtzig hat geschrieben:Garbage collection an sich ist für mich nichts Neues, da es in den Achtzigern schon in BASIC-Dialekten verwendet wurde. Ungewöhnlich daran ist nur, daß eben auch Bezeichner aus dem Namensraum verschinden, die noch von einer Unterklasse benötigt werden. Das halte ich für ein Manko. Für mich ist der kontrollierte Abbau genauso wichtig wie der kontrollierte Aufbau (und der Rest dazwischen). Der Destruktor von object könnte beispielsweise ein Signal setzen, daß das Objekt nun (irgendwann) aus dem Speicher entfernt werden kann. Damit wäre gewährleistet, daß alle abgeleiteten Destruktoren durchlaufen wurden.
Code in Destruktoren zu plazieren, ist sehr, sehr schlechter Stil. Eine absolutes no-go.

Bei Sprache wie Java, C# und eben auch Python ist der Zeitpunkt des Aufrufs von Destruktoren keinesfalls garantiert. Es kann sogar passieren, dass der Destruktor überhaupt nicht aufgerufen wird, wenn das Programm nur kurz läuft. Der Speicher wird anschließend vom Betriebssystem bereinigt.

Beispiel Dateiobjekt: Bei "statischen" Sprachen wie C++ oder Pascal kann man Dateiobjekte im Desktruktor schließen. Bei Python sollte man das nie tun, denn die Datei könnte ewig geöffnet bleiben. Bei einer lange laufenden Webanwendung könnte es z.B. passieren, dass die Datei tagelang geöffnet bleibt.

Deswegen besitzen alle Objekte, die interne Aufräumarbeiten erfordern, auch Methoden, um diese explizit durchzuführen (z.B. close bei socket und file Objekten).

Daraus folgt automatisch, dass wenn immer du die Notwenigkeit eines Destruktors siehst, dein Design fundamental falsch ist. Man programmiert in Python anders als in Pascal. Man muss bei Python anders denken!
Das Fazit ist jedenfalls, daß ich nun vom Hauptprogramm aus den geregelten Abbau vornehme, also alle Objekte, bei denen der Zeitpunkt der Freigabe nicht zufällig sein sollte, über eine Liste referenziere und die Referenzen in umgekehrter Reihenfolge freigebe. Das gleiche dann auch bei den Gruppenobjekten, die ihrerseits Objektreferenzen enthalten.
Mmmh, mit diesem spärlichen Informationen hört sich das ein bisschen an, als wolltest du Pascal-Paradigmen wie Destruktoren 1:1 in Python simulieren. Das geht mit ziemlicher Sicherheit schief... Bestenfalls endet es nur in miesem Code.
Achtzig
User
Beiträge: 5
Registriert: Freitag 11. Mai 2007, 10:41

Samstag 2. Juni 2007, 09:22

Nun, ich werde darüber nachdenken. Seit vielen Jahren bin ich es gewöhnt, alles selbstständig freizugeben, was bei Assembler sowieso zingend erforderlich ist. Ich bin ungewollterweise irgendwie etwas konservativ und habe leichte Schwierigkeiten, mich von Destruktoren zu verabschieden. Aber nun gut - keine Destruktoren :(

Mit freundlichen Grüßen
Achtzig
BlackJack

Samstag 2. Juni 2007, 09:50

Achtzig hat geschrieben:Andersrum gesehen könnte die Basisklasse aber dennoch solange bereitgehalten werden, bis eine abgeleitete Klasse aufgelöst ist.
Da abgeleitete Klassen eine Referenz auf ihre Basisklasse besitzen, ist das selbstverständlich der Fall. Darum funktioniert `b.__class__.__base__` auch immer. Dein Problem war nicht, dass die Basisklasse weg war, sondern das Modul war schon beseitigt.
Wenn ein Objekt nicht mehr referenziert wird und der beanspruchte Speicherbereich irgendwann durch garbage collection tatsächlich freigegeben wird, ist es mir egal, zu welchen Zeitpunkt das passiert. Wichtig ist, daß die ganze Destruktorenkette durchlaufen werden kann. Ein Objekt wird ja auch nicht feigegeben, solange es referenziert wird. Mir fehlt da ein bischen die dreidimensionale Auflösung, die ja einen wesendlichen Aspekt in der objektorientierten Programmierung darstellt.
Was meinst Du mit dreidimensionaler Auflösung?
Garbage collection an sich ist für mich nichts Neues, da es in den Achtzigern schon in BASIC-Dialekten verwendet wurde. Ungewöhnlich daran ist nur, daß eben auch Bezeichner aus dem Namensraum verschinden, die noch von einer Unterklasse benötigt werden. Das halte ich für ein Manko.
Der Name verschwindet nicht, er wurde nur an ein anderes Objekt (`None`) gebunden.

Das Problem ist, dass man bei Programmende irgendwie die Modulobjekte abräumen muss und, mit an Sicherheit grenzender Wahrscheinlichkeit in jedem nicht supertrivialen Programm, zyklische Abhängigkeiten bestehen. Und die muss man irgendwie durchbrechen. Bei Programmende und Modulen werden diese Zyklen deshalb mit "Gewalt" durchbrochen.
Der Destruktor von object könnte beispielsweise ein Signal setzen, daß das Objekt nun (irgendwann) aus dem Speicher entfernt werden kann. Damit wäre gewährleistet, daß alle abgeleiteten Destruktoren durchlaufen wurden.
`__del__()` wird erst aufgerufen, wenn ein Objekt nicht mehr Referenziert wird. Damit ist der Referenzzähler selber schon das gewünschte Signal. Und kann natürlich nicht aus der Methode gesetzt werden, die erst aufgerufen wird, wenn es bereits gesetzt *ist*. :-)
Das Fazit ist jedenfalls, daß ich nun vom Hauptprogramm aus den geregelten Abbau vornehme, also alle Objekte, bei denen der Zeitpunkt der Freigabe nicht zufällig sein sollte, über eine Liste referenziere und die Referenzen in umgekehrter Reihenfolge freigebe. Das gleiche dann auch bei den Gruppenobjekten, die ihrerseits Objektreferenzen enthalten.
Dann betreibst Du wahrscheinlich genau das Mikromanagement, welches man mit Garbage Collection eigentlich vermeiden will. Statt mit Listen und deren Freigabe kannst Du an der Stelle auch explizit eine `close()`- oder `release()`-Methode aufrufen, was den Sinn und Zweck an der Stelle wesentlich deutlicher macht.

Vergiss `__del__()`! Die Methode ist nicht wirklich zu gebrauchen. Es wird von der Sprache nicht garantiert wann die Methode aufgerufen wird und es *wird* garantiert, dass sie *nie* aufgerufen wird, wenn eine zyklische Abhängigkeit zwischen Objekten mit einer `__del__()`-Methode besteht. Und wenn man das ständig im Blick haben will, könnte man sich gleich ums Speichermanagement selbst kümmern.
Achtzig
User
Beiträge: 5
Registriert: Freitag 11. Mai 2007, 10:41

Samstag 2. Juni 2007, 10:28

Was meinst Du mit dreidimensionaler Auflösung?
Mit dreidimensional meine ich meine bildliche Vorstellung der Vererbung. Ich rufe von außen eine Methode auf, die im Inneren des Objektes bei Bedarf nach unten weitergereicht wird. Von Außen sehe ich nur die oberste Ebene des Klassenturms. Auf den Destruktor bezogen, fehlen die unteren Ebenen (jedenfalls dachte ich das). Ist dann aber wohl egal, wenn es nicht garantiert ist, daß er überhaupt aufgerufen wird.
`__del__()` wird erst aufgerufen, wenn ein Objekt nicht mehr Referenziert wird
Auch da werde ich mich natürlich umgewöhnen müssen. Wie lunar richtig bemerkte, bin ich, was OOP betrifft, sehr auf Pascal fixiert. Da wird der Destruktor aufgerufen, bevor die Referenz weg ist.
Vergiss `__del__()`! Die Methode ist nicht wirklich zu gebrauchen. Es wird von der Sprache nicht garantiert wann die Methode aufgerufen wird und es *wird* garantiert, dass sie *nie* aufgerufen wird ...
Werde jetzt wohl ein bis zwei Tage in tiefe Depressionen fallen, um mich danach an den ganzen Vorzügen von Python zu erfreuen :wink:

Ich danke Euch für Eure Eindringlichkeit und ausführlichen Erläuterungen.

Mit freundlichen Grüßen
Achtzig
Antworten