527 votes

Comment nettoyer correctement un objet Python ?

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 Python ne garantit pas l'existence de "variables globales" (données membres dans ce contexte ?) lorsque __del__() est invoquée. Si c'est le cas et que c'est la raison de l'exception, comment puis-je m'assurer que l'objet se détruit correctement ?

3 votes

En lisant ce que vous avez lié, les variables globales qui disparaissent ne semblent pas s'appliquer ici, à moins que vous ne parliez de la sortie du programme, pendant laquelle, selon ce que vous avez lié, il est POSSIBLE que le module os lui-même ait déjà disparu. Sinon, je ne pense pas que cela s'applique aux variables membres dans une méthode __del__().

3 votes

L'exception est levée bien avant que mon programme ne se termine. L'exception AttributeError que je reçois est due au fait que Python dit qu'il ne reconnaît pas self.files comme étant un attribut de Package. Il se peut que je me trompe, mais si par "globales", on n'entend pas les variables globales aux méthodes (mais peut-être locales à la classe), alors je ne sais pas ce qui cause cette exception. Google hints Python se réserve le droit de nettoyer les données des membres avant que __del__(self) ne soit appelé.

1 votes

Le code tel que posté semble fonctionner pour moi (avec Python 2.5). Pouvez-vous poster le code qui ne fonctionne pas - ou une version simplifiée (plus c'est simple, mieux c'est) qui provoque toujours l'erreur ?

700voto

Clint Miller Points 6339

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

Pour utiliser le with 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, quand quelqu'un veut utiliser votre classe, il fait ce qui suit :

with Package() as package_obj:
    # use package_obj

La variable package_obj sera une instance de type Package (c'est la valeur renvoyée par la fonction __enter__ méthode). Son site __exit__ sera automatiquement appelée, qu'une exception se produise ou non.

Vous pouvez même pousser cette approche un peu plus loin. Dans l'exemple ci-dessus, quelqu'un pourrait toujours instancier Package à l'aide de son constructeur sans utiliser l'attribut with clause. Vous ne voulez pas que cela se produise. Vous pouvez remédier à ce problème en créant une classe PackageResource qui définit l'attribut __enter__ y __exit__ méthodes. Ensuite, la classe Package serait définie strictement à l'intérieur de la classe __enter__ et retourné. De cette façon, l'appelant ne pourra jamais instancier la classe Package sans utiliser une méthode de type with déclaration :

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 l'utiliserez comme suit :

with PackageResource() as package_obj:
    # use package_obj

45 votes

Techniquement parlant, on pourrait appeler PackageResource().__enter__() explicitement et ainsi créer un Package qui ne serait jamais finalisé... mais il faudrait vraiment essayer de casser le code. Ce n'est probablement pas quelque chose dont il faut s'inquiéter.

4 votes

D'ailleurs, si vous utilisez Python 2.5, vous devrez faire du futuro Importez with_statement pour pouvoir utiliser l'instruction with.

3 votes

J'ai trouvé un article qui aide à montrer pourquoi __del__() agit comme il le fait et donne du crédit à l'utilisation d'une solution de gestion de contexte : andy-pearce.com/blog/posts/2013/Apr/python-destructor-drawbacks

18voto

Virgil Dupras Points 1603

Je ne pense pas qu'il soit possible que les membres de l'instance soient retirés avant que __del__ est appelé. Je pense que la raison de votre AttributeError particulière se trouve ailleurs (vous avez peut-être supprimé par erreur self.file ailleurs).

Cependant, comme les autres l'ont souligné, vous devez éviter d'utiliser __del__ . La raison principale en est que les instances avec __del__ ne seront pas collectés (ils ne seront libérés 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 fonctionnera. (Je peux me tromper sur tout ceci, il faudrait que je relise la documentation gc, mais je suis plutôt sûr que ça fonctionne comme ça).

6 votes

Objets avec __del__ peuvent être collectés si leur nombre de références à d'autres objets de la catégorie __del__ est égal à zéro et ils sont inaccessibles. Cela signifie que si vous avez un cycle de référence entre les objets avec __del__ aucun d'entre eux ne sera collecté. Tout autre cas, cependant, devrait être résolu comme prévu.

4 votes

"À partir de Python 3.4, les méthodes __del__() n'empêchent plus la collecte des cycles de référence et les globaux de module ne sont plus forcés à None pendant l'arrêt de l'interpréteur. Ce code devrait donc fonctionner sans problème sur CPython." - docs.python.org/3.6/library/

14voto

n3o59hf Points 315

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

__del__ sera appelé même si __init__ n'a pas été exécuté correctement ou a déclenché une exception.

Source :

2 votes

Cela semble très probable. La meilleure façon d'éviter ce problème en utilisant __del__ est de déclarer explicitement tous les membres au niveau de la classe, en s'assurant qu'ils existent toujours, même si les membres de la classe ne sont pas présents. __init__ échoue. Dans l'exemple donné, files = () pourrait fonctionner, bien que la plupart du temps, il suffirait d'assigner None ; dans les deux cas, vous devez toujours attribuer la valeur réelle dans le champ __init__ .

8voto

Unknown Points 22789

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

Editar

Essayez ç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 le fichier del dont l'existence est garantie au moment de l'appel. Le proxy weakref est destiné à 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 originale). Si ce n'est pas le cas, même s'il existe d'autres références à la variable, vous pouvez supprimer l'encapsulation du proxy.

2 votes

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 simplifié mon code cependant, il y a d'autres données que je dois nettoyer moi-même (c'est-à-dire que l'interpréteur ne saura pas comment nettoyer).

6voto

Bastien Léonard Points 18404

Il semble que la manière idiomatique de le faire soit de fournir un fichier close() (ou une méthode similaire), et l'appeler de manière explicite.

23 votes

C'est l'approche que j'utilisais auparavant, mais j'ai rencontré d'autres problèmes avec elle. Avec des exceptions lancées un peu partout par d'autres bibliothèques, j'ai besoin de l'aide de Python pour nettoyer le bazar en cas d'erreur. Plus précisément, j'ai besoin que Python appelle le destructeur pour moi, car sinon le code devient rapidement ingérable, et j'oublierai sûrement un point de sortie là où devrait se trouver un appel à .close().

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X