Comment nettoyer correctement un objet Python?

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

    # ...

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

__del__(self)ci-dessus échoue avec une exception AttributeError. Je comprends que Python ne garantit pas l'existence de "variables globales" (données de membre dans ce contexte?) Lorsqu'il __del__()est invoqué. Si tel est le cas et que c'est la raison de l'exception, comment puis-je m'assurer que l'objet se détruit correctement?

wilhelmtell
la source
3
En lisant ce que vous avez lié, les variables globales qui disparaissent ne semblent pas s'appliquer ici, sauf si vous parlez de la fin de votre programme, au cours de laquelle je suppose qu'en fonction de ce que vous avez lié, il pourrait être POSSIBLE que le module os lui-même soit déjà parti. Sinon, je ne pense pas que cela s'applique aux variables membres dans une méthode __del __ ().
Kevin Anderson
3
L'exception est levée bien avant la fin de mon programme. L'exception AttributeError que j'obtiens est Python disant qu'il ne reconnaît pas self.files comme étant un attribut de Package. Je peux me tromper, mais si par "globaux" ils ne signifient pas des variables globales aux méthodes (mais peut-être locales à la classe), alors je ne sais pas ce qui cause cette exception. Google suggère que Python se réserve le droit de nettoyer les données des membres avant d'appeler __del __ (self).
wilhelmtell
1
Le code tel que publié semble fonctionner pour moi (avec Python 2.5). Pouvez-vous publier le code réel qui échoue - ou une version simplifiée (la plus simple est la meilleure qui provoque toujours l'erreur?)
Silverfish
@ wilhelmtell pouvez-vous donner un exemple plus concret? Dans tous mes tests, le del destructor fonctionne parfaitement.
Inconnu
7
Si quelqu'un veut savoir: cet article explique pourquoi __del__ne pas l'utiliser comme contrepartie __init__. (C'est-à-dire que ce n'est pas un "destructeur" au sens où __init__c'est un constructeur.
franklin

Réponses:

619

Je recommande d'utiliser la withdéclaration de Python pour gérer les ressources qui doivent être nettoyées. Le problème avec l'utilisation d'une close()déclaration explicite est que vous devez vous inquiéter du fait que les gens oublient de l'appeler ou oublient de le placer dans un finallybloc pour éviter une fuite de ressources lorsqu'une exception se produit.

Pour utiliser l' withinstruction, créez une classe avec les méthodes suivantes:

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

Dans votre exemple ci-dessus, vous utiliseriez

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)

Ensuite, lorsque quelqu'un voulait utiliser votre classe, il procédait comme suit:

with Package() as package_obj:
    # use package_obj

La variable package_obj sera une instance de type Package (c'est la valeur retournée par la __enter__méthode). Sa __exit__méthode sera automatiquement appelée, qu'une exception se produise ou non.

Vous pourriez même aller plus loin dans cette approche. Dans l'exemple ci-dessus, quelqu'un pourrait toujours instancier Package en utilisant son constructeur sans utiliser la withclause. Vous ne voulez pas que cela se produise. Vous pouvez résoudre ce problème en créant une classe PackageResource qui définit les méthodes __enter__et __exit__. Ensuite, la classe Package serait définie strictement à l'intérieur de la __enter__méthode et renvoyée. De cette façon, l'appelant n'a jamais pu instancier la classe Package sans utiliser une withinstruction:

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

Vous utiliseriez ceci comme suit:

with PackageResource() as package_obj:
    # use package_obj
Clint Miller
la source
35
Techniquement parlant, on pourrait appeler PackageResource () .__ entrer __ () explicitement et ainsi créer un Package qui ne serait jamais finalisé ... mais ils devraient vraiment essayer de casser le code. Probablement pas de quoi s'inquiéter.
David Z
3
Soit dit en passant, si vous utilisez Python 2.5, vous devrez le faire à partir de la future importation with_statement pour pouvoir utiliser l'instruction with.
Clint Miller,
2
J'ai trouvé un article qui aide à montrer pourquoi __del __ () agit comme il le fait et donne foi à l'utilisation d'une solution de gestionnaire de contexte: andy-pearce.com/blog/posts/2013/Apr/python-destructor-drawbacks
eikonomega
2
Comment utiliser cette construction agréable et propre si vous voulez passer des paramètres? Je voudrais pouvoir fairewith Resource(param1, param2) as r: # ...
snooze92
4
@ snooze92, vous pouvez donner à la ressource une méthode __init__ qui stocke * args et ** kwargs en elle-même, puis les transmettre à la classe interne dans la méthode enter. Lorsque vous utilisez l'instruction with, __init__ est appelé avant __enter__
Brian Schlenker
48

La méthode standard consiste à utiliser 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)

Mais vous devez garder à l'esprit que toutes les instances créées persisteront Packagejusqu'à ce que Python soit terminé.

Démo utilisant le code ci-dessus enregistré sous 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...
ostrokach
la source
2
La bonne chose à propos de l'approche atexit.register est que vous n'avez pas à vous soucier de ce que l'utilisateur de la classe fait (ont-ils utilisé with? Ont-ils appelé explicitement __enter__?) L'inconvénient est bien sûr si vous avez besoin que le nettoyage se produise avant python sort, ça ne marchera pas. Dans mon cas, je me fiche que ce soit lorsque l'objet sort de la portée ou si ce n'est pas jusqu'à la sortie de python. :)
hlongmore
Puis-je utiliser entrée et sortie et également ajouter atexit.register(self.__exit__)?
myradio
@myradio Je ne vois pas en quoi cela serait utile? Vous ne pouvez pas exécuter toute la logique de nettoyage à l'intérieur __exit__et utiliser un gestionnaire de contexte? Prend également __exit__des arguments supplémentaires (c.-à-d. __exit__(self, type, value, traceback)), Vous devrez donc vous en procurer. Quoi qu'il en soit, il semble que vous devriez publier une question distincte sur SO, car votre cas d'utilisation semble inhabituel?
ostrokach
33

En annexe à la réponse de Clint , vous pouvez simplifier en PackageResourceutilisant contextlib.contextmanager:

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

Alternativement, mais probablement pas en Pythonic, vous pouvez remplacer 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):
        ...

et utilisez simplement with Package(...) as package.

Pour raccourcir les choses, nommez closeet utilisez votre fonction de nettoyage contextlib.closing, auquel cas vous pouvez soit utiliser la Packageclasse non modifiée via, with contextlib.closing(Package(...))soit la remplacer __new__par la plus simple.

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

Et ce constructeur est hérité, vous pouvez donc simplement hériter, par exemple

class SubPackage(Package):
    def close(self):
        pass
Tobias Kienzler
la source
1
C'est génial. J'aime particulièrement le dernier exemple. Malheureusement, nous ne pouvons pas éviter le passe-partout à quatre lignes de la Package.__new__()méthode. Ou peut-être que nous pouvons. Nous pourrions probablement définir soit un décorateur de classe, soit une métaclasse généralisant ce passe-partout pour nous. Aliments pour la pensée pythonique.
Cecil Curry
@CecilCurry Merci et bon point. Toute classe héritant de Packagedevrait également le faire (même si je ne l'ai pas encore testé), donc aucune métaclasse ne devrait être requise. Bien que j'ai trouvé des moyens assez curieux d'utiliser métaclasses dans le passé ...
Tobias Kienzler
@CecilCurry En fait, le constructeur est hérité, vous pouvez donc utiliser Package(ou mieux une classe nommée Closing) comme parent de classe au lieu de object. Mais ne me demandez pas comment l'héritage multiple gâche cela ...
Tobias Kienzler
17

Je ne pense pas qu'il soit possible par exemple de supprimer des membres avant d' __del__appeler. Je suppose que la raison de votre AttributeError particulière est ailleurs (peut-être que vous supprimez par erreur self.file ailleurs).

Cependant, comme les autres l'ont souligné, vous devez éviter de l'utiliser __del__. La raison principale en est que les instances avec __del__ne seront pas récupérées (elles ne seront libérées que lorsque leur refcount atteindra 0). Par conséquent, si vos instances sont impliquées dans des références circulaires, elles resteront en mémoire aussi longtemps que l'application s'exécutera. (Je me trompe peut-être cependant, je devrais relire les documents gc, mais je suis plutôt sûr que cela fonctionne comme ça).

Virgil Dupras
la source
5
Les objets avec __del__peuvent être récupérés si leur nombre de références à partir d'autres objets avec __del__est nul et s'ils ne sont pas accessibles. Cela signifie que si vous avez un cycle de référence entre les objets avec __del__, aucun de ceux-ci ne sera collecté. Cependant, tout autre cas doit être résolu comme prévu.
Collin
"À partir de Python 3.4, les méthodes __del __ () n'empêchent plus les cycles de référence d'être récupérés, et les globales des modules ne sont plus forcées à None pendant l'arrêt de l'interpréteur. Donc, ce code devrait fonctionner sans aucun problème sur CPython." - docs.python.org/3.6/library/…
Tomasz Gandor
14

Une meilleure alternative consiste à utiliser le fichier lowref.finalize . Voir les exemples dans Finalizer Objects et Comparaison des finaliseurs avec les méthodes __del __ () .

SCGH
la source
1
Utilisé aujourd'hui et cela fonctionne parfaitement, mieux que d'autres solutions. J'ai une classe de communication basée sur le multiprocessing qui ouvre un port série, puis j'ai une stop()méthode pour fermer les ports et join()les processus. Cependant, si le programme se ferme de façon inattendue, il stop()n'est pas appelé - j'ai résolu cela avec un finaliseur. Mais dans tous les cas, j'appelle _finalizer.detach()la méthode stop pour éviter de l'appeler deux fois (manuellement et plus tard par le finaliseur).
Bojan P.
3
OMI, c'est vraiment la meilleure réponse. Il combine la possibilité de nettoyer à la collecte des ordures avec la possibilité de nettoyer à la sortie. La mise en garde est que python 2.7 n'a pas de faiblesse ref.finalize.
hlongmore
12

Je pense que le problème pourrait être __init__s'il y a plus de code que celui indiqué?

__del__sera appelé même s'il __init__n'a pas été exécuté correctement ou a levé une exception.

La source

n3o59hf
la source
2
Cela semble très probable. La meilleure façon d'éviter ce problème lors de l'utilisation __del__est de déclarer explicitement tous les membres au niveau de la classe, en s'assurant qu'ils existent toujours, même en cas d' __init__échec. Dans l'exemple donné, files = ()cela fonctionnerait, bien que la plupart du temps, vous ne fassiez qu'assigner None; dans les deux cas, vous devez toujours affecter la valeur réelle à __init__.
Søren Løvborg
11

Voici un squelette de travail minimal:

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

Important: retournez-vous


Si vous êtes comme moi et que vous négligez la return selfpartie (de la bonne réponse de Clint Miller ), vous regarderez ce non-sens:

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

J'espère que cela aide la prochaine personne.

user2394284
la source
8

Enveloppez simplement votre destructeur avec une instruction try / except et il ne lèvera pas d'exception si vos globaux sont déjà éliminés.

Éditer

Essaye ça:

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

Il remplira la liste des fichiers dans la fonction del qui est garantie d'exister au moment de l'appel. Le proxy de faiblesse est d'empêcher Python ou vous-même de supprimer la variable self.files d'une manière ou d'une autre (si elle est supprimée, cela n'affectera pas la liste de fichiers d'origine). Si ce n'est pas le cas que cela est supprimé même s'il y a plus de références à la variable, vous pouvez supprimer l'encapsulation du proxy.

Inconnue
la source
2
Le problème est que si les données des membres ont disparu, il est trop tard pour moi. J'ai besoin de ces données. Voir mon code ci-dessus: j'ai besoin des noms de fichiers pour savoir quels fichiers supprimer. J'ai cependant simplifié mon code, il y a d'autres données dont j'ai besoin pour me nettoyer (c'est-à-dire que l'interprète ne saura pas comment nettoyer).
wilhelmtell
4

Il semble que la façon idiomatique de le faire est de fournir une close()méthode (ou similaire), et de l'appeler explicitement.

Bastien Léonard
la source
20
C'est l'approche que j'ai utilisée auparavant, mais j'ai rencontré d'autres problèmes avec. Avec des exceptions lancées partout par d'autres bibliothèques, j'ai besoin de l'aide de Python pour nettoyer le gâchis en cas d'erreur. Plus précisément, j'ai besoin de Python pour appeler le destructeur pour moi, car sinon le code devient rapidement ingérable, et j'oublierai sûrement un point de sortie où un appel à .close () devrait être.
wilhelmtell