web-dev-qa-db-fra.com

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 de membre dans ce contexte?) Lorsque __del__() est appelé. Si tel est le cas et que cela constitue la raison de l'exception, comment puis-je m'assurer que l'objet se détruit correctement?

427
wilhelmtell

Je vous recommande d'utiliser l'instruction with de Python pour gérer les ressources à nettoyer. Le problème avec l'utilisation d'une instruction close() explicite est que vous devez vous inquiéter des personnes qui oublient de l'appeler ou de la placer dans un bloc finally afin d'éviter une fuite de ressource lorsqu'une exception se produit.

Pour utiliser l'instruction 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, lorsque quelqu'un souhaite utiliser votre classe, il procède comme 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 méthode __enter__). Sa méthode __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 pouvait toujours instancier Package en utilisant son constructeur sans utiliser la clause with. 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 sera définie strictement dans la méthode __enter__ et retournée. De cette manière, l'appelant ne pourrait jamais instancier la classe Package sans utiliser l'instruction with:

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
559
Clint Miller

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

Cependant, gardez à l'esprit que cela persistera toutes les instances créées de Package jusqu'à ce que Python soit terminé.

Démo utilisant le code ci-dessus enregistré en tant que 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...
_
35
ostrokach

En annexe de réponse de Clint , vous pouvez simplifier PackageResource avec contextlib.contextmanager :

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

Sinon, bien que 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 votre fonction de nettoyage close et utilisez contextlib.closing , auquel cas vous pouvez utiliser la classe non modifiée Package via with contextlib.closing(Package(...)). ou substituer son ___new___ au 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
_
27
Tobias Kienzler

Je ne pense pas qu'il soit possible, par exemple, de supprimer des membres avant que __del__ soit appelé. Je suppose que la raison de votre AttributeError particulier est ailleurs (vous supprimez peut-être par erreur self.file ailleurs).

Cependant, comme les autres l'ont fait remarquer, vous devriez éviter d'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 nombre de références atteindra 0). Par conséquent, si vos instances sont impliquées dans des références circulaires, elles vivront en mémoire aussi longtemps que l'application s'exécutera. (Je me trompe peut-être à propos de tout cela, il faudrait que je relise les documents gc, mais je suis plutôt sûr que cela fonctionne comme ça).

16
Virgil Dupras

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 généré une exception.

Source

11
n3o59hf

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

9
SCGH

Enveloppez simplement votre destructeur avec une instruction try/except et il ne lèvera pas une exception si vos globals 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 va farcir la liste de fichiers dans la fonction del dont l'existence est garantie au moment de l'appel. Le proxy faible vise à empêcher Python, ou vous-même, de supprimer la variable self.files (si elle est supprimée, la liste de fichiers d'origine ne sera pas affectée). Si ce n'est pas le cas, cela est en train d'être supprimé alors qu'il y a plus de références à la variable, vous pouvez alors supprimer l'encapsulation de proxy.

8
Unknown

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: retourne soi-même


Si vous êtes comme moi et que vous oubliez la partie return self (de réponse correcte 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'ai passé une demi-journée à ce sujet. J'espère que ça aide la prochaine personne.

6
user2394284

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

5
Bastien Léonard