Como faço para limpar corretamente um objeto Python?

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

    # ...

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

__del__(self)acima falha com uma exceção AttributeError. Entendo que o Python não garante a existência de "variáveis ​​globais" (dados de membros neste contexto?) Quando __del__()é invocado. Se for esse o caso e esse for o motivo da exceção, como posso garantir que o objeto seja destruído corretamente?

wilhelmtell
fonte
3
Lendo o que você vinculou, as variáveis ​​globais desaparecendo parecem não se aplicar aqui, a menos que você esteja falando quando o programa está saindo, durante o qual acho que, de acordo com o que você vinculou, pode ser POSSÍVEL que o próprio módulo OS já se foi. Caso contrário, não acho que se aplique a variáveis ​​de membro em um método __del __ ().
Kevin Anderson
3
A exceção é lançada muito antes da saída do meu programa. A exceção AttributeError que recebo é o Python dizendo que não reconhece self.files como sendo um atributo do Package. Eu posso estar entendendo errado, mas se por "globais" eles não significam variáveis ​​globais para métodos (mas possivelmente locais para classe), não sei o que causa essa exceção. Dicas do Google O Python se reserva o direito de limpar os dados dos membros antes que __del __ (self) seja chamado.
Wilhelmtell
1
O código postado parece funcionar para mim (com o Python 2.5). Você pode postar o código real que está a falhar - ou simplificado (quanto mais simples melhor versão que ainda faz com que o erro?
Silverfish
@ wilhelmtell você pode dar um exemplo mais concreto? Em todos os meus testes, o del destructor funciona perfeitamente.
Desconhecido
7
Se alguém quiser saber: Este artigo descreve por __del__que não deve ser usado como contrapartida __init__. (Ou seja, não é um "destruidor" no sentido em que __init__é um construtor.
franklin

Respostas:

619

Eu recomendo usar a withdeclaração do Python para gerenciar recursos que precisam ser limpos. O problema com o uso de uma close()declaração explícita é que você precisa se preocupar com as pessoas que se esquecem de chamá-la ou se esquecem de colocá-la em um finallybloco para evitar que um recurso vaze quando ocorrer uma exceção.

Para usar a withinstrução, crie uma classe com os seguintes métodos:

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

No seu exemplo acima, você usaria

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)

Então, quando alguém quisesse usar sua classe, faria o seguinte:

with Package() as package_obj:
    # use package_obj

A variável package_obj será uma instância do tipo Package (é o valor retornado pelo __enter__método). Seu __exit__método será chamado automaticamente, independentemente de ocorrer ou não uma exceção.

Você pode até levar essa abordagem um passo adiante. No exemplo acima, alguém ainda pode instanciar o Package usando seu construtor sem usar a withcláusula Você não quer que isso aconteça. Você pode corrigir isso criando uma classe PackageResource que define o __enter__e __exit__métodos. Em seguida, a classe Package seria definida estritamente dentro do __enter__método e retornada. Dessa forma, o chamador nunca poderia instanciar a classe Package sem usar uma withinstrução:

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

Você usaria isso da seguinte maneira:

with PackageResource() as package_obj:
    # use package_obj
Clint Miller
fonte
35
Tecnicamente falando, pode-se chamar PackageResource () .__ digitar __ () explicitamente e, assim, criar um pacote que nunca seria finalizado ... mas eles realmente precisariam tentar quebrar o código. Provavelmente não é algo para se preocupar.
217 David Z
3
A propósito, se você estiver usando o Python 2.5, precisará fazer a importação futura with_statement para poder usar a instrução with.
Clint Miller
2
Encontrei um artigo que ajuda a mostrar por que __del __ () age dessa maneira e dá credibilidade ao uso de uma solução de gerenciador de contexto: andy-pearce.com/blog/posts/2013/Apr/python-destructor-drawbacks
eikonomega
2
Como usar essa construção agradável e limpa, se você deseja passar parâmetros? Eu gostaria de poder fazerwith Resource(param1, param2) as r: # ...
snooze92 20/01
4
@ snooze92, você pode dar ao Resource um método __init__ que armazena * args e ** kwargs em si mesmo e depois os passa para a classe interna no método enter. Ao usar o com a declaração, __init__ é chamado antes __enter__
Brian Schlenker
48

A maneira padrão é 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)

Mas você deve ter em mente que isso persistirá em todas as instâncias criadas Packageaté que o Python seja encerrado.

Demonstração usando o código acima salvo 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
fonte
2
O lado bom da abordagem atexit.register é que você não precisa se preocupar com o que o usuário da classe faz (eles usaram with? Eles explicitamente ligaram __enter__?) A desvantagem é obviamente se você precisar que a limpeza ocorra antes do python sai, não vai funcionar. No meu caso, não me importo se é quando o objeto sai do escopo ou se não é até a saída do python. :)
hlongmore
Posso usar entrar e sair e também adicionar atexit.register(self.__exit__)?
myradio
@myradio Não vejo como isso seria útil? Você não pode executar toda a lógica de limpeza interna __exit__e usar um gerenciador de contexto? Além disso, __exit__leva argumentos adicionais (ou seja __exit__(self, type, value, traceback)), então você precisaria contá-los. De qualquer maneira, parece que você deve postar uma pergunta separada no SO, porque seu caso de uso parece incomum?
ostrokach
33

Como um apêndice à resposta de Clint , você pode simplificar PackageResourceusando contextlib.contextmanager:

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

Como alternativa, embora provavelmente não seja Pythonic, você pode substituir 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 simplesmente use with Package(...) as package.

Para simplificar , nomeie sua função de limpeza closee use contextlib.closing; nesse caso, você pode usar a Packageclasse não modificada with contextlib.closing(Package(...))ou substituí __new__-la pela mais simples

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

E esse construtor é herdado, então você pode simplesmente herdar, por exemplo

class SubPackage(Package):
    def close(self):
        pass
Tobias Kienzler
fonte
1
Isso é incrível. Eu particularmente gosto do último exemplo. É lamentável que não possamos evitar o clichê de quatro linhas do Package.__new__()método, no entanto. Ou talvez possamos. Provavelmente, poderíamos definir um decorador de classe ou uma metaclasse que genere esse padrão para nós. Alimento para o pensamento pitônico.
perfil completo de Cecil Curry
@CecilCurry Obrigado, e bom argumento. Qualquer classe herdada de Packagetambém deve fazer isso (embora eu ainda não testei), portanto, nenhuma metaclasse deve ser necessária. Apesar de eu ter encontrado algumas maneiras muito curiosos para utilizar metaclasses no passado ...
Tobias KIENZLER
@CecilCurry Na verdade, o construtor é herdado, então você pode usar Package(ou melhor, uma classe nomeada Closing) como pai da classe em vez de object. Mas não me pergunte como messes herança múltipla com isso ...
Tobias KIENZLER
17

Eu não acho que é possível, por exemplo, remover membros antes de __del__ser chamado. Meu palpite seria que o motivo do seu AttributeError específico está em outro lugar (talvez você remova o self.file por engano em outro lugar).

No entanto, como os outros apontaram, você deve evitar o uso __del__. A principal razão para isso é que as instâncias com __del__não serão coletadas como lixo (elas serão liberadas somente quando a contagem de refechas chegar a 0). Portanto, se suas instâncias estiverem envolvidas em referências circulares, elas permanecerão na memória enquanto o aplicativo for executado. (Posso estar enganado sobre tudo isso, porém, eu precisaria ler os documentos do gc novamente, mas tenho certeza de que funciona assim).

Virgil Dupras
fonte
5
Os objetos com __del__podem ser coletados com lixo se a contagem de referência de outros objetos com __del__for zero e eles estiverem inacessíveis. Isso significa que se você tiver um ciclo de referência entre objetos com __del__, nenhum deles será coletado. Qualquer outro caso, no entanto, deve ser resolvido conforme o esperado.
Collin
"Começando com o Python 3.4, os métodos __del __ () não impedem mais que os ciclos de referência sejam coletados como lixo, e as globais do módulo não são mais forçadas a None durante o desligamento do intérprete. Portanto, esse código deve funcionar sem problemas no CPython." - docs.python.org/3.6/library/…
Tomasz Gandor em
14

Uma alternativa melhor é usar o fracoref.finalize . Veja os exemplos em Objetos do finalizador e Comparando finalizadores com os métodos __del __ () .

SCGH
fonte
1
Utilizado hoje e funciona perfeitamente, melhor do que outras soluções. Tenho uma classe de comunicador baseada em multiprocessamento que abre uma porta serial e, em seguida, tenho um stop()método para fechar as portas e join()os processos. No entanto, se o programa sair inesperadamente, stop()não será chamado - resolvi isso com um finalizador. Mas, em qualquer caso, eu chamo _finalizer.detach()o método stop para evitar chamá-lo duas vezes (manualmente e depois novamente pelo finalizador).
Bojan P.
3
IMO, esta é realmente a melhor resposta. Combina a possibilidade de limpeza na coleta de lixo com a possibilidade de limpeza na saída. A ressalva é que o python 2.7 não possui um fracoref.finalize.
Hlongmore 15/05/19
12

Eu acho que o problema poderia estar __init__se houver mais código do que o mostrado?

__del__será chamado mesmo quando __init__não for executado corretamente ou gerou uma exceção.

Fonte

n3o59hf
fonte
2
Parece muito provável. A melhor maneira de evitar esse problema ao usar __del__é declarar explicitamente todos os membros no nível da classe, garantindo que eles sempre existam, mesmo que __init__falhem. No exemplo dado, files = ()funcionaria, embora na maioria das vezes você acabasse de atribuir None; nos dois casos, você ainda precisará atribuir o valor real em __init__.
Søren Løvborg 8/03/13
11

Aqui está um esqueleto de trabalho 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: retornar a si mesmo


Se você é como eu, e ignora a return selfparte (da resposta correta de Clint Miller ), estará olhando para essa bobagem:

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

Espero que ajude a próxima pessoa.

user2394284
fonte
8

Apenas envolva seu destruidor com uma instrução try / except e ela não emitirá uma exceção se seus globais já estiverem descartados.

Editar

Tente o seguinte:

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

Ele irá preencher a lista de arquivos na função del que é garantida a existência no momento da chamada. O proxy weakref é impedir que o Python ou você mesmo exclua a variável self.files de alguma forma (se for excluída, não afetará a lista de arquivos original). Se não for o caso de isso estar sendo excluído, mesmo que haja mais referências à variável, você poderá remover o encapsulamento do proxy.

Desconhecido
fonte
2
O problema é que, se os dados dos membros desaparecerem, é tarde demais para mim. Eu preciso desses dados. Veja meu código acima: preciso dos nomes dos arquivos para saber quais arquivos remover. Eu simplifiquei meu código, porém, existem outros dados que eu preciso me limpar (ou seja, o intérprete não saberá como limpar).
Wilhelmtell
4

Parece que a maneira idiomática de fazer isso é fornecer um close()método (ou similar) e chamá-lo explicitamente.

Bastien Léonard
fonte
20
Essa é a abordagem que eu usei antes, mas tive outros problemas com ela. Com exceções lançadas em todo o lugar por outras bibliotecas, preciso da ajuda do Python para limpar a bagunça no caso de um erro. Especificamente, preciso que o Python chame o destruidor para mim, porque, caso contrário, o código se tornará rapidamente incontrolável, e certamente esquecerei um ponto de saída onde deveria ser uma chamada para .close ().
Wilhelmtell