Jak poprawnie wyczyścić obiekt Python?

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

    # ...

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

__del__(self)powyżej kończy się niepowodzeniem z wyjątkiem wyjątku AttributeError. Rozumiem, że Python nie gwarantuje istnienia „zmiennych globalnych” (danych członka w tym kontekście?), Gdy __del__()zostanie wywołany. Jeśli tak jest i to jest powód wyjątku, w jaki sposób mogę upewnić się, że obiekt zniszczy się prawidłowo?

wilhelmtell
źródło
3
Czytanie tego, co połączyłeś, wydaje się, że zmienne globalne nie mają tutaj zastosowania, chyba że mówisz o tym, kiedy program się kończy, podczas którego, jak sądzę, zgodnie z tym, co połączyłeś, MOŻLIWE jest, że sam moduł os już zniknął. W przeciwnym razie nie sądzę, że dotyczy to zmiennych składowych w metodzie __del __ ().
Kevin Anderson
3
Wyjątek został zgłoszony na długo przed zamknięciem mojego programu. Wyjątkiem AttributeError, który otrzymuję, jest Python, który mówi, że nie rozpoznaje self.files jako atrybutu Package. Mogę się mylić, ale jeśli przez „globals” nie oznaczają zmiennych globalnych dla metod (ale być może lokalnych dla klasy), to nie wiem, co powoduje ten wyjątek. Wskazówki Google Python zastrzega sobie prawo do czyszczenia danych członków przed wywołaniem __del __ (self).
wilhelmtell
1
Wydany kod wydaje się działać dla mnie (z Python 2.5). Czy możesz opublikować kod, który się nie udaje - lub uproszczony (im prostsza, tym lepsza wersja nadal powoduje błąd?
Silverfish
@ wilhelmtell czy możesz podać bardziej konkretny przykład? We wszystkich moich testach del destructor działa idealnie.
Nieznany
7
Jeśli ktoś chce wiedzieć: ten artykuł wyjaśnia, dlaczego __del__nie powinien być stosowany jako odpowiednik __init__. (Tj. Nie jest to „destruktor” w tym sensie, że __init__jest konstruktorem.
franklin

Odpowiedzi:

619

Polecam użycie withinstrukcji Pythona do zarządzania zasobami, które wymagają oczyszczenia. Problem z używaniem wyraźnego close()stwierdzenia polega na tym, że musisz się martwić, że ludzie w ogóle zapomną go nazwać lub zapomną umieścić go wfinally bloku, aby zapobiec wyciekom zasobów w przypadku wystąpienia wyjątku.

Aby użyć withinstrukcji, utwórz klasę przy użyciu następujących metod:

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

W powyższym przykładzie użyłbyś

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)

Następnie, gdy ktoś chciałby skorzystać z twojej klasy, zrobiłby to:

with Package() as package_obj:
    # use package_obj

Zmienna package_obj będzie instancją typu Package (jest to wartość zwracana przez __enter__metodę). Jego__exit__ metoda zostanie automatycznie wywołana, niezależnie od tego, czy wystąpi wyjątek.

Możesz nawet pójść o krok dalej. W powyższym przykładzie ktoś nadal może utworzyć instancję pakietu przy użyciu swojego konstruktora bez użycia withklauzuli. Nie chcesz, żeby tak się stało. Możesz to naprawić, tworząc klasę PackageResource, która definiuje metody __enter__i __exit__. Następnie klasa Package zostanie zdefiniowana ściśle wewnątrz __enter__metody i zwrócona. W ten sposób osoba dzwoniąca nigdy nie może utworzyć instancji klasy Package bez użycia withinstrukcji:

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

Użyłbyś tego w następujący sposób:

with PackageResource() as package_obj:
    # use package_obj
Clint Miller
źródło
35
Technicznie rzecz biorąc, można wywołać PackageResource () .__ jawnie wpisać __ (), a tym samym stworzyć pakiet, który nigdy nie zostanie sfinalizowany ... ale naprawdę będą musieli próbować złamać kod. Prawdopodobnie nie ma się czym martwić.
David Z
3
Nawiasem mówiąc, jeśli używasz Python 2.5, musisz zrobić z przyszłego importu with_statement, aby móc używać instrukcji with.
Clint Miller
2
Znalazłem artykuł, który pomaga pokazać, dlaczego __del __ () zachowuje się tak, jak działa i daje wiarygodność korzystania z rozwiązania do zarządzania kontekstem: andy-pearce.com/blog/posts/2013/Apr/python-destructor-drawbacks
eikonomega
2
Jak korzystać z tej ładnej i czystej konstrukcji, jeśli chcesz przekazać parametry? Chciałbym móc to zrobićwith Resource(param1, param2) as r: # ...
drzemka92
4
@ snooze92 możesz podać zasobowi metodę __init__, która przechowuje * args i ** kwargs w sobie, a następnie przekazuje je do klasy wewnętrznej w metodzie enter. Podczas korzystania z instrukcji with, __init__ jest wywoływany przed __enter__
Brian Schlenker
48

Standardowym sposobem jest użycie 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)

Należy jednak pamiętać, że spowoduje to zachowanie wszystkich utworzonych instancji do Packagemomentu zakończenia działania Pythona.

Demo przy użyciu powyższego kodu zapisanego jako 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
źródło
2
Zaletą podejścia atexit.register jest to, że nie musisz martwić się o to, co robi użytkownik klasy (czy używał with? Czy wyraźnie dzwonił __enter__?) Minusem jest oczywiście to, że jeśli potrzebujesz wyczyszczenia przed pythonem wychodzi, to nie zadziała. W moim przypadku nie obchodzi mnie, czy dzieje się tak, gdy obiekt wykracza poza zasięg, czy też nie, dopóki Python nie wyjdzie. :)
hlongmore
Czy mogę użyć Enter i Exit, a także dodać atexit.register(self.__exit__)?
myradio
@myradio Nie rozumiem, jak by to było przydatne? Czy nie możesz wykonać całej logiki czyszczenia __exit__i użyć menedżera kontekstu? Ponadto __exit__pobiera dodatkowe argumenty (tj. __exit__(self, type, value, traceback)), Więc musisz się za nie zgodzić. Tak czy inaczej, wygląda na to, że powinieneś zadać osobne pytanie na SO, ponieważ twoja skrzynka wygląda na nietypową?
ostrokach
33

Jako dodatek do odpowiedzi Clinta możesz uprościć, PackageResourceużywając contextlib.contextmanager:

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

Alternatywnie, choć prawdopodobnie nie jako Python, możesz zastąpić 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):
        ...

i po prostu użyj with Package(...) as package .

Aby wszystko było krótsze, nazwij swoją funkcję czyszczenia closei użyj contextlib.closing, w którym to przypadku możesz albo użyć niezmodyfikowanej Packageklasy, with contextlib.closing(Package(...))albo zastąpić ją __new__prostszą

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

I ten konstruktor jest dziedziczony, więc możesz po prostu odziedziczyć, np

class SubPackage(Package):
    def close(self):
        pass
Tobias Kienzler
źródło
1
To jest niesamowite. Szczególnie podoba mi się ostatni przykład. Niestety, nie możemy uniknąć czteroliniowej płyty kotłowej Package.__new__()metody. A może możemy. Prawdopodobnie moglibyśmy zdefiniować dekorator klasy lub metaklasę generującą dla nas tę płytę grzewczą. Jedzenie dla myśli Pythona.
Cecil Curry
@CecilCurry Dzięki i dobra uwaga. Każda klasa dziedzicząca po tym Packagerównież powinna to zrobić (chociaż jeszcze tego nie testowałem), więc nie powinna być wymagana żadna metaklasa. Chociaż w przeszłości znalazłem kilka ciekawych sposobów używania metaklas ...
Tobias Kienzler,
@CecilCurry W rzeczywistości konstruktor jest dziedziczony, więc możesz użyć Package(lub lepiej klasy o nazwie Closing) jako swojego rodzica klasy zamiast object. Ale nie pytaj mnie, jak wiele dziedziczenia łączy się z tym ...
Tobias Kienzler,
17

Nie sądzę, że możliwe jest na przykład usunięcie członków przed __del__wywołaniem. Domyślam się, że przyczyną konkretnego błędu AttributeError jest gdzie indziej (być może błędnie usuwasz plik self.file w innym miejscu).

Jednak, jak zauważyli inni, należy unikać używania __del__. Głównym tego powodem jest to, że instancje z __del__nie będą zbierane śmieci (zostaną one uwolnione dopiero, gdy ich licznik osiągnie 0). Dlatego jeśli instancje są zaangażowane w odwołania cykliczne, będą one istnieć w pamięci tak długo, jak aplikacja będzie działać. (Mogę się jednak mylić co do tego, musiałbym ponownie przeczytać dokumentację gc, ale raczej jestem pewien, że tak to działa).

Virgil Dupras
źródło
5
Obiekty z __del__mogą być odśmiecane, jeśli ich liczba referencyjna dla innych obiektów z __del__jest zero i są one nieosiągalne. Oznacza to, że jeśli masz cykl odniesienia między obiektami __del__, żaden z nich nie zostanie zebrany. Każdy inny przypadek powinien jednak zostać rozwiązany zgodnie z oczekiwaniami.
Collin
„Począwszy od Pythona 3.4, metody __del __ () nie zapobiegają już gromadzeniu śmieci przez cykle referencyjne, a globały modułów nie są już wymuszane na Brak podczas zamykania interpretera. Dlatego ten kod powinien działać bez żadnych problemów na CPython.” - docs.python.org/3.6/library/…
Tomasz Gandor
14

Lepszą alternatywą jest użycie słabyref.finalizuj . Zobacz przykłady w Finalizer Objects i Porównanie finalizatorów z metodami __del __ () .

SCGH
źródło
1
Użyłem tego dzisiaj i działa bezbłędnie, lepiej niż inne rozwiązania. Mam klasę komunikatora opartą na wieloprocesorach, która otwiera port szeregowy, a następnie mam stop()metodę zamknięcia portów i join()procesów. Jednak jeśli programy nieoczekiwanie wychodzą, stop()nie jest wywoływane - rozwiązałem to za pomocą finalizatora. Ale w każdym razie _finalizer.detach()wywołuję metodę stop, aby zapobiec podwójnemu wywołaniu (ręcznie i później przez finalizator).
Bojan P.
3
IMO, to naprawdę najlepsza odpowiedź. Łączy w sobie możliwość czyszczenia przy zbieraniu śmieci z możliwością czyszczenia przy wyjściu. Zastrzeżenie polega na tym, że Python 2.7 nie ma słabyref.finalize.
hlongmore
12

Myślę, że problem może występować, __init__jeśli jest więcej kodów niż pokazano?

__del__zostanie wywołany, nawet jeśli __init__nie został poprawnie wykonany lub nie zgłosił wyjątku.

Źródło

n3o59hf
źródło
2
Brzmi bardzo prawdopodobne. Najlepszym sposobem na uniknięcie tego problemu podczas używania __del__jest jawne zadeklarowanie wszystkich członków na poziomie klasy, upewniając się, że zawsze istnieją, nawet jeśli __init__zawiodą. W podanym przykładzie files = ()działałoby, choć w większości wystarczyłoby przypisać None; w obu przypadkach nadal musisz przypisać rzeczywistą wartość w __init__.
Søren Løvborg,
11

Oto minimalny działający szkielet:

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

Ważne: zwróć siebie


Jeśli jesteś podobny do mnie i przeoczysz tę return selfczęść ( poprawnej odpowiedzi Clinta Millera ), będziesz patrzył na ten nonsens:

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

Mam nadzieję, że to pomoże następnej osobie.

użytkownik2394284
źródło
8

Po prostu zawiń destruktor instrukcją try / wyjątek, a nie wyrzuci on wyjątku, jeśli twoje globały są już usunięte.

Edytować

Spróbuj tego:

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

Wypełni listę plików w funkcji del, która z pewnością istnieje w momencie wywołania. Słaby serwer proxy ma uniemożliwić Pythonowi lub tobie usunięcie zmiennej self.files w jakiś sposób (jeśli zostanie ona usunięta, nie wpłynie to na oryginalną listę plików). Jeśli nie jest tak, że jest on usuwany, mimo że istnieje więcej odwołań do zmiennej, możesz usunąć enkapsulację proxy.

Nieznany
źródło
2
Problem polega na tym, że jeśli dane członka znikną, jest dla mnie za późno. Potrzebuję tych danych. Zobacz mój kod powyżej: Potrzebuję nazw plików, aby wiedzieć, które pliki należy usunąć. Uprościłem jednak mój kod, są inne dane, które muszę sam wyczyścić (tzn. Interpreter nie będzie wiedział, jak to wyczyścić).
wilhelmtell
4

Wydaje się, że idiomatycznym sposobem na to jest podanie close()metody (lub podobnej) i wyraźne jej wywołanie.

Bastien Léonard
źródło
20
Takie podejście stosowałem wcześniej, ale napotkałem na inne problemy. Z wyjątkami zgłaszanymi wszędzie przez inne biblioteki, potrzebuję pomocy Pythona w usuwaniu bałaganu w przypadku błędu. W szczególności potrzebuję Pythona, aby wywołać dla mnie destruktor, ponieważ w przeciwnym razie kod stanie się szybko niemożliwy do zarządzania i na pewno zapomnę punkt wyjścia, w którym powinno być wywołanie funkcji .close ().
wilhelmtell