Hinweis: Obwohl JavaScript für diese Website nicht unbedingt erforderlich ist, werden Ihre Interaktionsmöglichkeiten mit den Inhalten eingeschränkt sein. Bitte aktivieren Sie JavaScript für das volle Erlebnis.

Vorgeschlagene Verbesserungen zur Modulbereinigung

Vorgeschlagene Verbesserungen zur Modulbereinigung

Ich experimentiere mit einem besseren Weg zur Bereinigung am Ende eines Ausführungszyklus. Ohne die Implementierung einer echten GC kann ich es nie zu 100% richtig machen, aber ich kann eine vorhersagbare Reihe von Regeln implementieren, die auf praktischen Beobachtungen basieren und die meisten tatsächlich beobachteten Probleme lösen werden.

Hier ist mein Vorschlag. Am Ende dieser Nachricht liste ich einige potenzielle Probleme mit dem Vorschlag auf und bitte um Feedback. Dies wird wahrscheinlich in Python 1.5.1 implementiert.

Inhalt

Überarbeitete Version

Basierend auf einigen Kommentaren, die ich erhalten habe, und weiteren Überlegungen habe ich dies seit meinem Web-Post zu diesem Thema etwas geändert. Wesentliche Änderungen sind im Text durch [kursive Anmerkungen in eckigen Klammern] gekennzeichnet.

Algorithmen

Wenn ein Python-Interpreter gelöscht wird, werden seine Variablen und Module "sorgfältig bereinigt" in einer teilweise spezifizierten Reihenfolge. Die Operation "sorgfältig bereinigen" ist unten definiert; sie löscht effektiv die Variablen des Moduls in einer teilweise spezifizierten Reihenfolge.

  • M1. Vor allem anderem werden die folgenden Variablen auf None gesetzt (nicht notwendigerweise in dieser Reihenfolge)
    • __builtin__._
    • sys.exc_{type,value,traceback}
    • sys.last_{type,value,traceback}
    • sys.path
    • sys.argv
    • sys.ps1, sys.ps2
    • sys.exitfunc
    [path, argv, ps1, ps2 und exitfunc sind neu in dieser Liste.]

  • M2. Die drei Standard-E/A-Dateien (sys.stdin, sys.stdout und sys.stderr) werden auf ihre ursprünglichen Werte zurückgesetzt (die als sys.__stdin__, sys.__stdout__ bzw. sys.__stderr__ gespeichert werden, wenn der Interpreter startet). Wenn einer der ursprünglichen Werte nicht verfügbar ist, wird das entsprechende Objekt auf None gesetzt. [Neu.]

  • M3. Lösche Modul __main__ sorgfältig, bevor andere Module gelöscht werden. [Dies wurde früher nach dem nächsten Schritt getan.]

  • M4. Wiederhole die Schleife über alle Module, suche nach Modulen mit einer Referenzanzahl von eins. Jedes Modul mit einer Referenzanzahl von eins wird sorgfältig bereinigt. Die Schleife stoppt, wenn keine weiteren Module mit einer Referenzanzahl von eins gefunden werden. Die Module __builtin__ und sys werden von der Schleife ausgeschlossen.

  • M5. Lösche alle verbleibenden Module sorgfältig, außer __builtin__ und sys.

  • M6. Lösche sys sorgfältig.

  • M7. Lösche __builtin__ sorgfältig.

Um ein Modul sorgfältig zu bereinigen, werden die folgenden Schritte durchgeführt

  • C1. In einer Reihenfolge, die durch das Hashing der Namen im Wörterbuch bestimmt wird, werden alle Namen auf None gesetzt, die mit genau einem Unterstrich beginnen.

  • C2. In einer Reihenfolge, die durch das Hashing der Namen im Wörterbuch bestimmt wird, werden alle Namen auf None gesetzt, außer __builtins__. [Dies war früher "alle Namen, die nicht mit zwei oder mehr Unterstrichen beginnen".]

  • [Gelöschter Schritt: In einer Reihenfolge, die durch das Hashing der Namen im Wörterbuch bestimmt wird, lösche alle verbleibenden Namen aus dem Wörterbuch des Moduls (dies geschieht durch einen Aufruf von __dict__.clear()).]

  • C3. Das Modul selbst wird in der Liste der Module (sys.modules) durch None ersetzt.

[Neu.] Die Schritte C1-C2 werden auch verwendet, wenn ein Modul freigegeben wird. Während Module im Allgemeinen nicht an Zyklen beteiligt sind (außer bei gegenseitig rekursiven Importen), ist das Wörterbuch eines Moduls im Allgemeinen an einem Zyklus beteiligt, da jede Funktion und Methode, die im Modul definiert ist, auf ihr __dict__ verweist und diese Funktionen und Methoden im Allgemeinen von diesem __dict__ erreichbar sind. Wenn also ein Modul gelöscht wird, lösche ich explizit sein __dict__ sorgfältig. (Dies wurde immer getan, nur nicht "sorgfältig".)

Motivation

M1 wird durchgeführt, da diese Variablen häufig Verstecke für Benutzerwerte sind und sie in der vorgeschlagenen Reihenfolge zu spät kämen. (Tatsächlich haben fast alle gemeldeten Probleme mit Destruktoren, die nicht wie erwartet aufgerufen werden, mit diesen zu tun.)

M3 ist vorhanden, da __main__ konzeptionell die "Wurzel" des Programms ist – wenn es nicht von anderen Modulen importiert wird, würde es ohnehin zuerst durch Schritt M4 gelöscht, aber wenn es woanders importiert wird, ist das Löschen von __main__ eine plausible Möglichkeit, einen Gleichstand zu brechen.

M4 ist eine explizite Garbage-Collection-Schleife – sie löscht alle Module, die von keinen anderen Modulen referenziert werden, sondern nur von der Modultabelle (sys.modules) selbst. Sie löscht möglicherweise nicht alle Module, wenn es gegenseitige Importe gibt; die verbleibenden Schritte kümmern sich darum.

M5 ist erforderlich, um gegenseitig rekursive Importe zu behandeln, die Zyklen erzeugen, sodass M4 nicht alles löscht.

Die Sonderbehandlung von __builtin__ und sys ist darauf zurückzuführen, dass diese vom Interpreter implizit bei vielen Operationen referenziert werden; __builtin__ enthält natürlich alle eingebauten Funktionen und Ausnahmen; sys enthält die Standard-E/A-Dateien, die implizit bei verschiedenen E/A-Operationen referenziert werden. Daher werden sie in M2 und M4 ausgeschlossen. __builtin__ wird zuletzt gelöscht, da es die grundlegendsten und fundamentalsten Werte enthält.

Die besondere Sorgfalt bei der Bereinigung des Wörterbuchs eines Moduls ist erforderlich, da es eine grundlegende zyklische Referenz gibt, wenn ein Modul eine Python-Funktion oder -Klasse definiert. Ein Funktions-Objekt enthält eine Referenz auf das 'globals'-Objekt der Funktion, das das __dict__ des Moduls ist, das sie definiert. Da das __dict__ normalerweise eine Referenz auf die Funktion enthält, gibt es einen Zyklus, der gebrochen werden muss, sonst würde das __dict__ nie vom Garbage Collector freigegeben werden.

Beachten Sie, dass eine referenzzählbasierte Lösung innerhalb eines Moduls nicht funktioniert, da Referenzen zwischen Funktionen über Namen und nicht über Werte erfolgen – zwei sich gegenseitig rekursiv aufrufende Funktionen können immer noch beide eine Referenzanzahl von eins haben, da sie gegenseitig nach Namen suchen.

C1 ist ein Versuch, eine Möglichkeit für ein Modul zu schaffen, Globale zu definieren, die vor allem anderen im Modul gelöscht werden. Da importierte Modul- oder Funktionsnamen im Allgemeinen nicht mit einem Unterstrich beginnen, bedeutet dies, dass solche Objekte garantiert werden können, dass alle importierten Module oder Funktionen noch existieren, wenn sie gelöscht werden – vorausgesetzt natürlich, dass die einzige Referenz darauf im Modul liegt. (Dieser Schritt ist bereits in 1.5 wie veröffentlicht implementiert.)

C2 löscht die verbleibenden Objekte, lässt aber die "internen globalen Variablen" __builtins__ unangetastet – dies verhindert das Problem, das die 1.5-Version hat, wo z. B. die Verwendung von "None" in einem Destruktor einen NameError auslöst!

C3 entfernt die Referenz auf das Modul aus der Modultabelle auf eine Weise, die einen späteren Import desselben Moduls fehlschlagen lässt. (Es ist möglich, dass Benutzercode diesen Eintrag löscht und dennoch einen völlig neuen Import startet – aber wenn sie so schlau sind, verdienen sie, was sie bekommen.)

Probleme und Fragen

P1. Wenn alle Verwendungen eines Moduls M die Form ``from M import ...'' haben, hat das Modul M eine Referenzanzahl von 1. Daher wird es in Schritt M1 gelöscht. Dies macht alle bis auf die trivialsten Funktionen, die im Modul definiert sind (die vermutlich noch von anderen Modulen referenziert werden), nutzlos, da die importierten Module und Funktionen, die sie möglicherweise benötigen, alle aus ihren Globals gelöscht werden. Ein einfaches Gegenmittel ist natürlich, nicht ``from M import ...'' zu verwenden, aber das klingt, als könnte es zu einem FAQ werden... Das Problem ist, dass ich keinen besseren Weg kenne – wegen der zyklischen Referenzen zwischen Funktionen und dem __dict__ ihres Moduls kann ich die Referenzanzahl des __dict__ in Schritt M1 nicht verwenden. Ich halte das für akzeptabel – dieses Verhalten existierte auch in 1.4 und früheren Versionen. (Es wurde vorgeschlagen, eine Referenz hinzuzufügen, die z. B. ".module" heißt, im importierenden Modul, um die Abhängigkeit anzuzeigen und dieses Problem zu verhindern; obwohl dies die Aufgabe schön erledigen mag, zögere ich, es zu implementieren, da es introspektive Werkzeuge verwirren könnte.)