Wie bereinige ich ein Python-Objekt korrekt?

462
class Package:
    def __init__(self):
        self.files = []

    # ...

    def __del__(self):
        for file in self.files:
            os.unlink(file)

__del__(self)oben schlägt mit einer AttributeError-Ausnahme fehl. Ich verstehe, dass Python beim Aufrufen nicht die Existenz von "globalen Variablen" (Mitgliedsdaten in diesem Kontext?) Garantiert__del__() . Wenn dies der Fall ist und dies der Grund für die Ausnahme ist, wie stelle ich sicher, dass das Objekt ordnungsgemäß zerstört wird?

wilhelmtell
quelle
3
Wenn Sie lesen, was Sie verknüpft haben, scheinen globale Variablen hier nicht zuzutreffen, es sei denn, Sie sprechen über das Beenden Ihres Programms. Währenddessen denke ich, dass es nach Ihren Verknüpfungen möglicherweise möglich ist, dass das Betriebssystemmodul selbst bereits nicht mehr vorhanden ist. Ansonsten glaube ich nicht, dass dies für Mitgliedsvariablen in einer __del __ () -Methode gilt.
Kevin Anderson
3
Die Ausnahme wird lange vor dem Beenden meines Programms ausgelöst. Die AttributeError-Ausnahme, die ich erhalte, ist Python, das sagt, dass self.files nicht als Attribut von Package erkannt wird. Ich verstehe das vielleicht falsch, aber wenn mit "global" keine Variablen global für Methoden (aber möglicherweise lokal für Klassen) gemeint sind, weiß ich nicht, was diese Ausnahme verursacht. Google weist darauf hin, dass Python sich das Recht vorbehält, Mitgliedsdaten zu bereinigen, bevor __del __ (self) aufgerufen wird.
Wilhelmtell
1
Der veröffentlichte Code scheint für mich zu funktionieren (mit Python 2.5). Können Sie den tatsächlichen Code veröffentlichen, der fehlschlägt - oder einen vereinfachten (je einfacher, desto besser die Version, die den Fehler immer noch verursacht?)
Silverfish
@ wilhelmtell kannst du ein konkreteres Beispiel geben? In all meinen Tests funktioniert der Del Destructor perfekt.
Unbekannt
7
Wenn jemand wissen möchte: In diesem Artikel wird erläutert, warum __del__nicht als Gegenstück zu verwendet werden sollte __init__. (Dh es ist kein "Destruktor" im Sinne __init__eines Konstruktors.
Franklin

Antworten:

619

Ich würde empfehlen, die Python- withAnweisung zum Verwalten von Ressourcen zu verwenden, die bereinigt werden müssen. Das Problem bei der Verwendung einer expliziten close()Anweisung besteht darin, dass Sie sich Sorgen machen müssen, dass Personen vergessen, sie überhaupt aufzurufen oder sie in einem finallyBlock zu platzieren, um ein Ressourcenleck zu verhindern, wenn eine Ausnahme auftritt.

withErstellen Sie eine Klasse mit den folgenden Methoden, um die Anweisung zu verwenden:

  def __enter__(self)
  def __exit__(self, exc_type, exc_value, traceback)

In Ihrem obigen Beispiel würden Sie verwenden

class Package:
    def __init__(self):
        self.files = []

    def __enter__(self):
        return self

    # ...

    def __exit__(self, exc_type, exc_value, traceback):
        for file in self.files:
            os.unlink(file)

Wenn dann jemand Ihre Klasse benutzen wollte, würde er Folgendes tun:

with Package() as package_obj:
    # use package_obj

Die Variable package_obj ist eine Instanz vom Typ Package (dies ist der von der __enter__Methode zurückgegebene Wert ). Die __exit__Methode wird automatisch aufgerufen, unabhängig davon, ob eine Ausnahme auftritt oder nicht.

Sie könnten diesen Ansatz sogar noch einen Schritt weiter gehen. Im obigen Beispiel könnte jemand Package weiterhin mit seinem Konstruktor instanziieren, ohne die withKlausel zu verwenden. Du willst nicht, dass das passiert. Sie können dies beheben, indem Sie eine PackageResource-Klasse erstellen, die die Methoden __enter__und definiert __exit__. Dann würde die Package-Klasse streng innerhalb der __enter__Methode definiert und zurückgegeben. Auf diese Weise konnte der Aufrufer die Package-Klasse niemals instanziieren, ohne eine withAnweisung zu verwenden:

class PackageResource:
    def __enter__(self):
        class Package:
            ...
        self.package_obj = Package()
        return self.package_obj

    def __exit__(self, exc_type, exc_value, traceback):
        self.package_obj.cleanup()

Sie würden dies wie folgt verwenden:

with PackageResource() as package_obj:
    # use package_obj
Clint Miller
quelle
35
Technisch gesehen könnte man PackageResource () .__ enter __ () explizit aufrufen und so ein Paket erstellen, das niemals finalisiert werden würde ... aber sie müssten wirklich versuchen, den Code zu brechen. Wahrscheinlich kein Grund zur Sorge.
David Z
3
Übrigens, wenn Sie Python 2.5 verwenden, müssen Sie ab dem zukünftigen Import with_statement ausführen, um die with-Anweisung verwenden zu können.
Clint Miller
2
Ich habe einen Artikel gefunden, der zeigt, warum __del __ () so handelt, wie er funktioniert, und der Verwendung einer Kontextmanagerlösung Glaubwürdigkeit verleiht: andy-pearce.com/blog/posts/2013/Apr/python-destructor-drawbacks
eikonomega
2
Wie verwende ich dieses schöne und saubere Konstrukt, wenn ich Parameter übergeben möchte? Ich würde gerne in der Lage sein zu tunwith Resource(param1, param2) as r: # ...
snooze92
4
@ snooze92 Sie könnten der Ressource eine __init__ -Methode geben, die * args und ** kwargs in self speichert und sie dann in der enter-Methode an die innere Klasse weitergibt. Bei Verwendung der with-Anweisung wird __init__ vor __enter__ aufgerufen
Brian Schlenker
48

Die Standardmethode ist atexit.register:

# package.py
import atexit
import os

class Package:
    def __init__(self):
        self.files = []
        atexit.register(self.cleanup)

    def cleanup(self):
        print("Running cleanup...")
        for file in self.files:
            print("Unlinking file: {}".format(file))
            # os.unlink(file)

Beachten Sie jedoch, dass alle erstellten Instanzen so lange bestehen bleiben, Packagebis Python beendet wird.

Demo mit dem oben als package.py gespeicherten Code :

$ python
>>> from package import *
>>> p = Package()
>>> q = Package()
>>> q.files = ['a', 'b', 'c']
>>> quit()
Running cleanup...
Unlinking file: a
Unlinking file: b
Unlinking file: c
Running cleanup...
ostrokach
quelle
2
Das Schöne am atexit.register-Ansatz ist, dass Sie sich keine Gedanken darüber machen müssen, was der Benutzer der Klasse tut (haben sie verwendet with? Haben sie explizit aufgerufen __enter__?). Der Nachteil ist natürlich, wenn die Bereinigung vor Python erfolgen muss Ausgänge, es wird nicht funktionieren. In meinem Fall ist es mir egal, ob das Objekt den Gültigkeitsbereich verlässt oder erst, wenn Python beendet wird. :)
hlongmore
Kann ich enter und exit verwenden und auch hinzufügen atexit.register(self.__exit__)?
Myradio
@myradio Ich sehe nicht, wie das nützlich wäre? Können Sie nicht die gesamte darin enthaltene Bereinigungslogik ausführen __exit__und einen Kontextmanager verwenden? Auch __exit__nimmt zusätzliche Argumente (dh __exit__(self, type, value, traceback)), so müssen Sie würde für diejenigen accout. In jedem Fall sollten Sie eine separate Frage zu SO stellen, da Ihr Anwendungsfall ungewöhnlich erscheint.
Ostrokach
33

Als Anhang zu Clint Antwort , können Sie vereinfachen PackageResourcemit contextlib.contextmanager:

@contextlib.contextmanager
def packageResource():
    class Package:
        ...
    package = Package()
    yield package
    package.cleanup()

Alternativ, obwohl wahrscheinlich nicht als Pythonic, können Sie Folgendes überschreiben Package.__new__:

class Package(object):
    def __new__(cls, *args, **kwargs):
        @contextlib.contextmanager
        def packageResource():
            # adapt arguments if superclass takes some!
            package = super(Package, cls).__new__(cls)
            package.__init__(*args, **kwargs)
            yield package
            package.cleanup()

    def __init__(self, *args, **kwargs):
        ...

und einfach benutzen with Package(...) as package.

Um die Dinge kürzer zu machen, benennen Sie Ihre Bereinigungsfunktion closeund verwenden Sie sie contextlib.closing. In diesem Fall können Sie entweder die unveränderte PackageKlasse über verwenden with contextlib.closing(Package(...))oder __new__die einfachere überschreiben

class Package(object):
    def __new__(cls, *args, **kwargs):
        package = super(Package, cls).__new__(cls)
        package.__init__(*args, **kwargs)
        return contextlib.closing(package)

Und dieser Konstruktor wird geerbt, so dass Sie einfach erben können, z

class SubPackage(Package):
    def close(self):
        pass
Tobias Kienzler
quelle
1
Das ist fantastisch. Das letzte Beispiel gefällt mir besonders gut. Es ist jedoch bedauerlich, dass wir die vierzeilige Kesselplatte der Package.__new__()Methode nicht umgehen können . Oder vielleicht können wir. Wir könnten wahrscheinlich entweder einen Klassendekorateur oder eine Metaklasse definieren, die dieses Boilerplate für uns generisiert. Nahrung für pythonische Gedanken.
Cecil Curry
@ CecilCurry Danke und guter Punkt. Jede Klasse, die von erbt, Packagesollte dies auch tun (obwohl ich das noch nicht getestet habe), daher sollte keine Metaklasse erforderlich sein. Obwohl ich habe ein paar ziemlich merkwürdige Weise zu nutzen metaclasses in der Vergangenheit ... gefunden
Tobias Kienzler
@CecilCurry Tatsächlich wird der Konstruktor geerbt, sodass Sie stattdessen Package(oder besser eine Klasse mit dem Namen Closing) als übergeordnetes Element verwenden können object. Aber fragen Sie mich nicht, wie die mehrfache Vererbung dies durcheinander bringt ...
Tobias Kienzler
17

Ich denke nicht, dass es möglich ist, dass beispielsweise Mitglieder entfernt werden, bevor sie __del__aufgerufen werden. Ich vermute, dass der Grund für Ihren speziellen AttributeError woanders liegt (vielleicht entfernen Sie self.file fälschlicherweise woanders).

Wie die anderen betonten, sollten Sie jedoch die Verwendung vermeiden __del__. Der Hauptgrund dafür ist, dass Instanzen mit __del__nicht mit Müll gesammelt werden (sie werden erst freigegeben, wenn ihr Refcount 0 erreicht). Wenn Ihre Instanzen an Zirkelverweisen beteiligt sind, bleiben sie daher so lange im Speicher, wie die Anwendung ausgeführt wird. (Ich kann mich bei all dem irren, ich müsste die gc-Dokumente noch einmal lesen, aber ich bin mir ziemlich sicher, dass es so funktioniert).

Vergil Dupras
quelle
5
Objekte mit __del__können durch Müll gesammelt werden, wenn ihre Referenzanzahl von anderen Objekten mit __del__Null ist und sie nicht erreichbar sind. Dies bedeutet, wenn Sie einen Referenzzyklus zwischen Objekten mit haben __del__, wird keiner von diesen erfasst. Jeder andere Fall sollte jedoch wie erwartet gelöst werden.
Collin
"Ab Python 3.4 verhindern __del __ () -Methoden nicht mehr, dass Referenzzyklen durch Speicherbereinigung erfasst werden, und Modulglobale werden beim Herunterfahren des Interpreters nicht mehr auf None gezwungen. Daher sollte dieser Code unter CPython problemlos funktionieren." - docs.python.org/3.6/library/…
Tomasz Gandor
14

Eine bessere Alternative ist die Verwendung von schwachref.finalize . Siehe die Beispiele unter Finalizer-Objekte und Vergleichen von Finalisierern mit __del __ () -Methoden .

SCGH
quelle
1
Wird heute verwendet und funktioniert einwandfrei, besser als andere Lösungen. Ich habe eine Multiprozessor-basierte Kommunikatorklasse, die eine serielle Schnittstelle öffnet, und dann habe ich eine stop()Methode, um die Ports und join()die Prozesse zu schließen. Wenn das Programm jedoch unerwartet beendet wird, stop()wird es nicht aufgerufen - das habe ich mit einem Finalizer gelöst. Aber auf jeden Fall rufe ich _finalizer.detach()die stop-Methode auf, um zu verhindern, dass sie zweimal aufgerufen wird (manuell und später erneut vom Finalizer).
Bojan P.
3
IMO, das ist wirklich die beste Antwort. Es kombiniert die Möglichkeit der Reinigung bei der Müllabfuhr mit der Möglichkeit der Reinigung am Ausgang. Die Einschränkung ist, dass Python 2.7 keine schwache ref.finalize hat.
Hlongmore
12

Ich denke, das Problem könnte darin liegen, __init__dass mehr Code als gezeigt vorhanden ist.

__del__wird auch dann aufgerufen, wenn __init__es nicht richtig ausgeführt wurde oder eine Ausnahme ausgelöst hat.

Quelle

n3o59hf
quelle
2
Klingt sehr wahrscheinlich. Der beste Weg, um dieses Problem bei der Verwendung zu vermeiden, __del__besteht darin, alle Mitglieder auf Klassenebene explizit zu deklarieren und sicherzustellen, dass sie immer vorhanden sind, auch wenn dies __init__fehlschlägt. In dem gegebenen Beispiel files = ()würde funktionieren, obwohl Sie meistens nur zuweisen würden None; In beiden Fällen müssen Sie noch den tatsächlichen Wert in zuweisen __init__.
Søren Løvborg
11

Hier ist ein minimales Arbeitsskelett:

class SkeletonFixture:

    def __init__(self):
        pass

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        pass

    def method(self):
        pass


with SkeletonFixture() as fixture:
    fixture.method()

Wichtig: kehre selbst zurück


Wenn Sie wie ich sind und den return selfTeil (von Clint Millers korrekter Antwort ) übersehen , werden Sie auf diesen Unsinn starren:

Traceback (most recent call last):
  File "tests/simplestpossible.py", line 17, in <module>                                                                                                                                                          
    fixture.method()                                                                                                                                                                                              
AttributeError: 'NoneType' object has no attribute 'method'

Hoffe es hilft der nächsten Person.

user2394284
quelle
8

Schließen Sie Ihren Destruktor einfach mit einer try / exception-Anweisung ein, und es wird keine Ausnahme ausgelöst, wenn Ihre Globals bereits entsorgt sind.

Bearbeiten

Versuche dies:

from weakref import proxy

class MyList(list): pass

class Package:
    def __init__(self):
        self.__del__.im_func.files = MyList([1,2,3,4])
        self.files = proxy(self.__del__.im_func.files)

    def __del__(self):
        print self.__del__.im_func.files

Die Dateiliste wird in die del- Funktion eingefügt, die zum Zeitpunkt des Aufrufs garantiert vorhanden ist. Der schwache Ref-Proxy soll verhindern, dass Python oder Sie selbst die Variable self.files irgendwie löschen (wenn sie gelöscht wird, hat dies keine Auswirkungen auf die ursprüngliche Dateiliste). Wenn dies nicht der Fall ist, wird dies gelöscht, obwohl mehr Verweise auf die Variable vorhanden sind, können Sie die Proxy-Kapselung entfernen.

Unbekannt
quelle
2
Das Problem ist, dass es für mich zu spät ist, wenn die Mitgliedsdaten weg sind. Ich brauche diese Daten. Siehe meinen Code oben: Ich benötige die Dateinamen, um zu wissen, welche Dateien entfernt werden sollen. Ich habe meinen Code jedoch vereinfacht. Es gibt andere Daten, die ich selbst bereinigen muss (dh der Interpreter weiß nicht, wie er bereinigt werden soll).
Wilhelmtell
4

Es scheint, dass der idiomatische Weg, dies zu tun, darin besteht, eine close()Methode (oder eine ähnliche) bereitzustellen und sie explizit aufzurufen.

Bastien Léonard
quelle
20
Dies ist der Ansatz, den ich zuvor verwendet habe, aber ich bin auf andere Probleme damit gestoßen. Mit Ausnahmen, die von anderen Bibliotheken überall ausgelöst werden, benötige ich Pythons Hilfe, um das Chaos im Fehlerfall zu beseitigen. Insbesondere muss Python den Destruktor für mich aufrufen, da der Code sonst schnell nicht mehr verwaltet werden kann und ich sicherlich einen Exit-Punkt vergessen werde, an dem ein Aufruf von .close () erfolgen sollte.
Wilhelmtell