Come ripulisco correttamente un oggetto Python?


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

    # ...

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

__del__(self)sopra fallisce con un'eccezione AttributeError. Capisco che Python non garantisce l'esistenza di "variabili globali" (dati dei membri in questo contesto?) Quando __del__()viene invocato. Se questo è il caso e questo è il motivo dell'eccezione, come posso assicurarmi che l'oggetto si distrugga correttamente?


3
Leggendo ciò che hai collegato, le variabili globali che vanno via non sembrano applicarsi qui a meno che tu non stia parlando quando il tuo programma è in uscita, durante il quale immagino in base a ciò che hai collegato potrebbe essere POSSIBILE che il modulo OS stesso sia già sparito. Altrimenti, non penso che si applichi alle variabili membro in un metodo __del __ ().
Kevin Anderson,

3
L'eccezione viene generata molto prima che il mio programma venga chiuso. L'eccezione AttributeError che ottengo è Python che dice che non riconosce i file self come attributi del pacchetto. Potrei sbagliarmi, ma se per "globali" non significano variabili globali per metodi (ma possibilmente locali per classe), allora non so cosa causi questa eccezione. Google suggerisce che Python si riserva il diritto di ripulire i dati dei membri prima che venga chiamato __del __ (self).
Wilhelmtell,

1
Il codice pubblicato sembra funzionare per me (con Python 2.5). Riesci a pubblicare il codice effettivo che non funziona - o un semplificato (più semplice è la versione migliore che causa ancora l'errore?
Silverfish

@ Wilhelmtell puoi fare un esempio più concreto? In tutti i miei test, il del destructor funziona perfettamente.
Sconosciuto

7
Se qualcuno vuole sapere: questo articolo spiega perché __del__non dovrebbe essere usato come controparte di __init__. (Vale a dire, non è un "distruttore" nel senso che __init__è un costruttore.
Franklin

Risposte:


619

Consiglio di utilizzare la withdichiarazione di Python per la gestione delle risorse che devono essere ripulite. Il problema con l'uso di un'istruzione esplicita close()è che devi preoccuparti che le persone dimentichino di chiamarlo o dimentichino di inserirlo in un finallyblocco per evitare una perdita di risorse quando si verifica un'eccezione.

Per utilizzare l' withistruzione, creare una classe con i seguenti metodi:

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

Nel tuo esempio sopra, useresti

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)

Quindi, quando qualcuno volesse usare la tua classe, farebbe quanto segue:

with Package() as package_obj:
    # use package_obj

La variabile package_obj sarà un'istanza di tipo Package (è il valore restituito dal __enter__metodo). Il suo __exit__metodo verrà chiamato automaticamente, indipendentemente dal fatto che si verifichi o meno un'eccezione.

Potresti anche prendere questo approccio un ulteriore passo avanti. Nell'esempio sopra, qualcuno potrebbe ancora creare un'istanza di Package usando il suo costruttore senza usare la withclausola. Non vuoi che ciò accada. È possibile risolvere questo problema creando una classe PackageResource che definisce i metodi __enter__e __exit__. Quindi, la classe Package sarebbe definita rigorosamente all'interno del __enter__metodo e restituita. In questo modo, il chiamante non potrebbe mai creare un'istanza della classe Package senza usare withun'istruzione:

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()

Lo useresti come segue:

with PackageResource() as package_obj:
    # use package_obj

35
Tecnicamente parlando, si potrebbe chiamare PackageResource () .__ inserire __ () esplicitamente e quindi creare un pacchetto che non sarebbe mai stato finalizzato ... ma dovrebbero davvero provare a rompere il codice. Probabilmente non è qualcosa di cui preoccuparsi.
David Z,

3
A proposito, se stai usando Python 2.5, dovrai eseguire l' importazione with_statement in futuro per poter usare l'istruzione with.
Clint Miller,

2
Ho trovato un articolo che aiuta a mostrare perché __del __ () agisce come fa e dà credito all'utilizzo di una soluzione di gestione del contesto: andy-pearce.com/blog/posts/2013/Apr/python-destructor-drawbacks
eikonomega

2
Come usare quel costrutto bello e pulito se vuoi passare parametri? Mi piacerebbe poterlo farewith Resource(param1, param2) as r: # ...
snooze92,

4
@ snooze92 potresti dare alla risorsa un metodo __init__ che memorizza * args e ** kwargs in sé, e poi li passa alla classe interna nel metodo enter. Quando si utilizza l'istruzione with, __init__ viene chiamato prima __enter__
Brian Schlenker,

48

Il modo standard è usare 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)

Ma dovresti tenere presente che questo persisterà tutte le istanze create Packagefino a quando Python non sarà terminato.

Demo usando il codice sopra salvato come package.py :

$ 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...

2
La cosa bella dell'approccio atexit.register è che non devi preoccuparti di ciò che fa l'utente della classe (hanno usato with? Hanno chiamato esplicitamente __enter__?) L'aspetto negativo è ovviamente se hai bisogno che la pulizia avvenga prima di Python esce, non funzionerà. Nel mio caso, non mi interessa se è quando l'oggetto esce dal campo di applicazione o se non lo è fino alla chiusura di Python. :)
hlongmore

Posso usare Enter e Exit e anche aggiungere atexit.register(self.__exit__)?
Myradio,

@myradio Non vedo come sarebbe utile? Non riesci a eseguire tutta la logica di pulizia all'interno __exit__e utilizzare un gestore di contesto? Inoltre, __exit__accetta argomenti aggiuntivi (ad es. __exit__(self, type, value, traceback)), Quindi è necessario accout per quelli. Ad ogni modo, sembra che dovresti pubblicare una domanda separata su SO, perché il tuo caso d'uso sembra insolito?
ostrokach il

33

Come appendice alla risposta di Clint , puoi semplificare PackageResourceusando contextlib.contextmanager:

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

In alternativa, sebbene probabilmente non come Pythonic, puoi sovrascrivere 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):
        ...

e semplicemente usare with Package(...) as package.

Per rendere le cose più brevi, dai un nome alla funzione di pulizia closee usa contextlib.closing, nel qual caso puoi usare la Packageclasse non modificata tramite with contextlib.closing(Package(...))o sovrascriverla __new__con la più semplice

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

E questo costruttore è ereditato, quindi puoi semplicemente ereditare, ad es

class SubPackage(Package):
    def close(self):
        pass

1
Questo è bellissimo. Mi piace particolarmente l'ultimo esempio. È un peccato che non possiamo evitare la piastra a quattro righe del Package.__new__()metodo, tuttavia. O forse possiamo. Probabilmente potremmo definire un decoratore di classe o una metaclasse che generano quella placca per noi. Cibo per il pensiero Pythonic.
Cecil Curry,

@CecilCurry Grazie e buon punto. Anche qualsiasi classe che eredita Packagedovrebbe farlo (anche se non l'ho ancora testato), quindi non dovrebbe essere richiesta alcuna metaclasse. Anche se ho ho trovato alcuni modi piuttosto curiosi per utilizzare metaclassi in passato ...
Tobias Kienzler

@CecilCurry In realtà, il costruttore è ereditato, quindi è possibile utilizzare Package(o meglio una classe denominata Closing) come genitore della classe anziché object. Ma non chiedermi in che modo l'ereditarietà multipla incasina questo ...
Tobias Kienzler,

17

Non penso che sia possibile rimuovere i membri di esempio prima che __del__venga chiamato. La mia ipotesi sarebbe che il motivo del tuo particolare AttributeError sia da qualche altra parte (forse rimuovi erroneamente il file selfwhere altrove).

Tuttavia, come hanno sottolineato gli altri, dovresti evitare di usarlo __del__. Il motivo principale di ciò è che le istanze con __del__non verranno raccolte in modo inutile (verranno liberate solo quando il loro conto di conto raggiunge 0). Pertanto, se le istanze sono coinvolte in riferimenti circolari, rimarranno in memoria per tutto il tempo in cui l'applicazione viene eseguita. (Potrei sbagliarmi su tutto questo, però, dovrei leggere di nuovo i documenti gc, ma sono piuttosto sicuro che funzioni così).


5
Gli oggetti con __del__possono essere garbage collection se il loro riferimento conta da altri oggetti con __del__zero e non sono raggiungibili. Questo significa che se hai un ciclo di riferimento tra oggetti con __del__, nessuno di questi verrà raccolto. Qualsiasi altro caso, tuttavia, dovrebbe essere risolto come previsto.
Collin,

"A partire da Python 3.4, i metodi __del __ () non impediscono più la raccolta differenziata dei cicli di riferimento e i globuli di modulo non sono più costretti a None durante l'arresto dell'interprete. Quindi questo codice dovrebbe funzionare senza problemi su CPython." - docs.python.org/3.6/library/…
Tomasz Gandor il

14

Un'alternativa migliore è usare weakref.finalize . Vedi gli esempi in Oggetti finalizzatore e Confronto di finalizzatori con i metodi __del __ () .


1
Usato oggi e funziona perfettamente, meglio di altre soluzioni. Ho una classe di comunicazione basata su multiprocessing che apre una porta seriale e quindi ho un stop()metodo per chiudere le porte e join()i processi. Tuttavia, se il programma si chiude inaspettatamente stop()non viene chiamato, l'ho risolto con un finalizzatore. Ma in ogni caso chiamo _finalizer.detach()il metodo stop per evitare di chiamarlo due volte (manualmente e successivamente dal finalizzatore).
Bojan P.

3
IMO, questa è davvero la risposta migliore. Combina la possibilità di ripulire alla raccolta dei rifiuti con la possibilità di ripulire all'uscita. L'avvertenza è che Python 2.7 non ha weakref.finalize.
hlongmore,

12

Penso che il problema potrebbe essere __init__se c'è più codice di quello mostrato?

__del__verrà chiamato anche quando __init__non è stato eseguito correttamente o ha generato un'eccezione.

fonte


2
Sembra molto probabile. Il modo migliore per evitare questo problema quando si usa __del__è dichiarare esplicitamente tutti i membri a livello di classe, assicurandosi che esistano sempre, anche se __init__falliscono. Nell'esempio dato, files = ()avrebbe funzionato, anche se per lo più avresti semplicemente assegnato None; in entrambi i casi, è comunque necessario assegnare il valore reale in __init__.
Søren Løvborg

11

Ecco uno scheletro di lavoro minimo:

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()

Importante: restituire se stessi


Se sei come me e trascuri la return selfparte (della risposta corretta di Clint Miller ), starai fissando questa assurdità:

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

Spero che aiuti la prossima persona.


8

Avvolgi semplicemente il distruttore con un'istruzione try / tranne e non genererà un'eccezione se i tuoi globi sono già eliminati.

modificare

Prova questo:

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

Riempirà l'elenco dei file nella funzione del che è garantita al momento della chiamata. Il proxy weakref serve a impedire a Python o all'utente di eliminare in qualche modo la variabile self.files (se viene eliminata, non influirà sull'elenco dei file originale). In caso contrario, anche se ci sono più riferimenti alla variabile, è possibile rimuovere l'incapsulamento del proxy.


2
Il problema è che se i dati dei membri scompaiono è troppo tardi per me. Ho bisogno di quei dati. Vedi il mio codice sopra: ho bisogno dei nomi dei file per sapere quali file rimuovere. Ho semplificato il mio codice, tuttavia, ci sono altri dati che devo ripulire da me stesso (cioè l'interprete non saprà come ripulirlo).
Wilhelmtell,

4

Sembra che il modo idiomatico per farlo sia quello di fornire un close()metodo (o simile) e chiamarlo esplicitamente.


20
Questo è l'approccio che ho usato prima, ma ho riscontrato altri problemi. Con le eccezioni gettate ovunque da altre librerie, ho bisogno dell'aiuto di Python per ripulire il casino in caso di errore. In particolare, ho bisogno che Python chiami il distruttore per me, perché altrimenti il ​​codice diventa rapidamente ingestibile e sicuramente dimenticherò un punto di uscita dove dovrebbe essere una chiamata a .close ().
Wilhelmtell,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.