¿Cómo limpio correctamente un objeto Python?

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

    # ...

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

__del__(self)arriba falla con una excepción AttributeError. Entiendo que Python no garantiza la existencia de "variables globales" (¿datos de miembros en este contexto?) Cuando __del__()se invoca. Si ese es el caso y esta es la razón de la excepción, ¿cómo me aseguro de que el objeto se destruya correctamente?

Wilhelmtell
fuente
3
Al leer lo que vinculó, las variables globales que desaparecen no parecen aplicarse aquí a menos que esté hablando de cuándo sale su programa, durante el cual supongo que de acuerdo con lo que vinculó, es POSIBLE que el módulo OS ya no esté. De lo contrario, no creo que se aplique a las variables miembro en un método __del __ ().
Kevin Anderson
3
La excepción se produce mucho antes de que mi programa salga. La excepción AttributeError que obtengo es Python diciendo que no reconoce self.files como un atributo de Package. Puede que me esté equivocando, pero si por "globales" no se refieren a variables globales a los métodos (pero posiblemente locales a la clase), entonces no sé qué causa esta excepción. Google sugiere que Python se reserva el derecho de limpiar los datos de los miembros antes de llamar a __del __ (self).
wilhelmtell
1
El código publicado parece funcionar para mí (con Python 2.5). Se puede publicar el código real que está fallando - o una simplificado (cuanto más simple mejor versión que todavía hace que el error?
pececillo de plata
@ wilhelmtell ¿puedes dar un ejemplo más concreto? En todas mis pruebas, la del destructor funciona perfectamente.
Desconocido
77
Si alguien quiere saber: Este artículo explica por __del__qué no debe usarse como contraparte de __init__. (Es decir, no es un "destructor" en el sentido de que __init__es un constructor.
franklin

Respuestas:

619

Recomiendo usar la withdeclaración de Python para administrar los recursos que necesitan ser limpiados. El problema con el uso de una close()declaración explícita es que debe preocuparse de que las personas se olviden de llamarlo o se olviden de colocarlo en un finallybloque para evitar una pérdida de recursos cuando se produce una excepción.

Para usar la withinstrucción, cree una clase con los siguientes métodos:

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

En su ejemplo anterior, usaría

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)

Luego, cuando alguien quisiera usar tu clase, haría lo siguiente:

with Package() as package_obj:
    # use package_obj

La variable package_obj será una instancia de tipo Package (es el valor devuelto por el __enter__método). Se __exit__llamará automáticamente a su método, independientemente de si se produce o no una excepción.

Incluso podría llevar este enfoque un paso más allá. En el ejemplo anterior, alguien aún podría crear una instancia de Package usando su constructor sin usar la withcláusula. No quieres que eso suceda. Puede solucionar esto creando una clase PackageResource que defina los métodos __enter__y __exit__. Luego, la clase Package se definiría estrictamente dentro del __enter__método y se devolvería. De esa manera, la persona que llama nunca podría crear una instancia de la clase Package sin usar una withdeclaración:

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

Usarías esto de la siguiente manera:

with PackageResource() as package_obj:
    # use package_obj
Clint Miller
fuente
35
Técnicamente hablando, uno podría llamar a PackageResource () .__ enter __ () explícitamente y así crear un paquete que nunca se finalizaría ... pero realmente tendrían que estar tratando de romper el código. Probablemente no es algo de qué preocuparse.
David Z
3
Por cierto, si está utilizando Python 2.5, deberá hacerlo desde una futura importación with_statement para poder usar la instrucción with.
Clint Miller
2
Encontré un artículo que ayuda a mostrar por qué __del __ () actúa de la manera que lo hace y da crédito al uso de una solución de administrador de contexto: andy-pearce.com/blog/posts/2013/Apr/python-destructor-drawbacks
eikonomega
2
¿Cómo usar esa construcción agradable y limpia si quieres pasar parámetros? Me gustaría poder hacerlowith Resource(param1, param2) as r: # ...
snooze92
44
@ snooze92 podría darle al Recurso un método __init__ que almacena * args y ** kwargs en sí mismo, y luego los pasa a la clase interna en el método enter. Cuando se usa la instrucción with, se llama a __init__ antes de __enter__
Brian Schlenker
48

La forma estándar es usar 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)

Pero debe tener en cuenta que esto persistirá en todas las instancias creadas Packagehasta que Python finalice.

Demostración utilizando el código anterior guardado como 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
fuente
2
Lo bueno del enfoque atexit.register es que no tiene que preocuparse por lo que hace el usuario de la clase (¿usaron with? ¿Llamaron explícitamente __enter__?) La desventaja es, por supuesto, si necesita que se realice la limpieza antes de Python salidas, no funcionará. En mi caso, no me importa si es cuando el objeto está fuera de alcance o si no es hasta que Python salga. :)
hlongmore
¿Puedo usar entrar y salir y también agregar atexit.register(self.__exit__)?
myradio
@myradio ¿No veo cómo sería útil? ¿No puedes realizar toda la lógica de limpieza en el interior __exit__y usar un administrador de contexto? Además, __exit__toma argumentos adicionales (es decir __exit__(self, type, value, traceback)), por lo que necesitaría buscarlos. De cualquier manera, parece que debería publicar una pregunta por separado en SO, porque su caso de uso parece inusual.
Ostrokach
33

Como apéndice a la respuesta de Clint , puede simplificar el PackageResourceuso de contextlib.contextmanager:

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

Alternativamente, aunque probablemente no sea Pythonic, puede anular 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):
        ...

y simplemente usar with Package(...) as package.

Para acortar las cosas, asigne un nombre a su función de limpieza closey úsela contextlib.closing, en cuyo caso puede usar la Packageclase no modificada with contextlib.closing(Package(...))o anularla __new__a la más simple.

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

Y este constructor se hereda, por lo que simplemente puede heredar, por ejemplo

class SubPackage(Package):
    def close(self):
        pass
Tobias Kienzler
fuente
1
Esto es asombroso Particularmente me gusta el último ejemplo. Sin Package.__new__()embargo, es lamentable que no podamos evitar las repeticiones de cuatro líneas del método. O tal vez podamos. Probablemente podríamos definir un decorador de clases o una metaclase que genérico para nosotros. Alimento para el pensamiento pitónico.
Cecil Curry
@CecilCurry Gracias, y buen punto. Cualquier clase que herede Packagetambién debería hacer esto (aunque todavía no lo he probado), por lo que no se requiere una metaclase. A pesar de que he encontrado algunas formas muy curiosas para utilizar metaclases en el pasado ...
Tobias KIENZLER
@CecilCurry En realidad, el constructor se hereda, por lo que puede usar Package(o mejor una clase llamada Closing) como su padre de clase en lugar de object. Pero no me pregunten cómo la herencia múltiple se
equivoca
17

No creo que sea posible eliminar miembros antes de que __del__se llame. Supongo que la razón de su AttributeError particular está en otro lugar (tal vez elimine por error el archivo self en otro lugar).

Sin embargo, como señalaron los demás, debe evitar su uso __del__. La razón principal de esto es que las instancias con __del__no se recolectarán basura (solo se liberarán cuando su recuento llegue a 0). Por lo tanto, si sus instancias están involucradas en referencias circulares, vivirán en la memoria mientras se ejecute la aplicación. (Sin embargo, puedo estar equivocado acerca de todo esto, tendría que leer los documentos de gc nuevamente, pero estoy bastante seguro de que funciona así).

Virgil Dupras
fuente
55
Los objetos con __del__pueden recolectarse basura si su recuento de referencia de otros objetos con __del__es cero y no se puede alcanzarlos. Esto significa que si tiene un ciclo de referencia entre objetos con __del__, ninguno de esos será recolectado. Cualquier otro caso, sin embargo, debe resolverse como se esperaba.
Collin
"Comenzando con Python 3.4, los métodos __del __ () ya no impiden que se recolecten basura los ciclos de referencia, y los módulos globales ya no se ven obligados a Ninguno durante el apagado del intérprete. Por lo tanto, este código debería funcionar sin problemas en CPython". - docs.python.org/3.6/library/…
Tomasz Gandor
14

Una mejor alternativa es usar weakref.finalize . Vea los ejemplos en Objetos finalizadores y Comparación de finalizadores con los métodos __del __ () .

SCGH
fuente
1
Usó esto hoy y funciona perfectamente, mejor que otras soluciones. Tengo una clase de comunicador basada en multiprocesamiento que abre un puerto serie y luego tengo un stop()método para cerrar los puertos y join()los procesos. Sin embargo, si el programa se cierra inesperadamente, stop()no se llama, lo resolví con un finalizador. Pero en cualquier caso, llamo _finalizer.detach()al método de detención para evitar llamarlo dos veces (manualmente y luego nuevamente por el finalizador).
Bojan P.
3
OMI, esta es realmente la mejor respuesta. Combina la posibilidad de limpiar en la recolección de basura con la posibilidad de limpiar en la salida. La advertencia es que python 2.7 no tiene debilref.finalize.
hlongmore
12

Creo que el problema podría estar __init__si hay más código del que se muestra.

__del__se llamará incluso cuando __init__no se haya ejecutado correctamente o haya lanzado una excepción.

Fuente

n3o59hf
fuente
2
Suena muy probable. La mejor manera de evitar este problema cuando se usa __del__es declarar explícitamente a todos los miembros a nivel de clase, asegurando que siempre existan, incluso si __init__falla. En el ejemplo dado, files = ()funcionaría, aunque en su mayoría solo se asignaría None; en cualquier caso, aún necesita asignar el valor real en __init__.
Søren Løvborg
11

Aquí hay un esqueleto de trabajo mínimo:

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: volver a sí mismo


Si eres como yo y pasas por alto la return selfparte (de la respuesta correcta de Clint Miller ), estarás mirando estas tonterías:

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

Espero que ayude a la próxima persona.

usuario2394284
fuente
8

Simplemente envuelva su destructor con una declaración try / except y no arrojará una excepción si sus globals ya están eliminados.

Editar

Prueba esto:

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

Rellenará la lista de archivos en la función del que se garantiza que existe en el momento de la llamada. El proxy weakref es evitar que Python, o usted mismo, elimine la variable self.files de alguna manera (si se elimina, no afectará la lista de archivos original). Si no es el caso de que esto se esté eliminando aunque haya más referencias a la variable, puede eliminar la encapsulación del proxy.

Desconocido
fuente
2
El problema es que si los datos de los miembros se han ido, es demasiado tarde para mí. Necesito esos datos Vea mi código arriba: necesito los nombres de archivo para saber qué archivos eliminar. Sin embargo, simplifiqué mi código, hay otros datos que necesito limpiar yo mismo (es decir, el intérprete no sabrá cómo limpiar).
wilhelmtell
4

Parece que la forma idiomática de hacer esto es proporcionar un close()método (o similar) y llamarlo explícitamente.

Bastien Léonard
fuente
20
Este es el enfoque que usé antes, pero me encontré con otros problemas. Con excepciones lanzadas por todas partes por otras bibliotecas, necesito la ayuda de Python para limpiar el desorden en caso de error. Específicamente, necesito que Python llame al destructor por mí, porque de lo contrario el código se volverá rápidamente inmanejable, y seguramente olvidaré un punto de salida donde debería estar una llamada a .close ().
wilhelmtell