Bagaimana cara membersihkan objek Python dengan benar?

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

    # ...

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

__del__(self)di atas gagal dengan pengecualian AttributeError. Saya mengerti Python tidak menjamin keberadaan "variabel global" (data anggota dalam konteks ini?) Ketika __del__()dipanggil. Jika itu yang terjadi dan ini adalah alasan pengecualian, bagaimana cara memastikan objek rusak dengan benar?

wilhelmtell
sumber
3
Membaca apa yang Anda tautkan, variabel global yang hilang tampaknya tidak berlaku di sini kecuali jika Anda berbicara tentang kapan program Anda keluar, di mana saya kira menurut apa yang Anda tautkan itu MUNGKIN bahwa modul os itu sendiri sudah hilang. Kalau tidak, saya tidak berpikir itu berlaku untuk variabel anggota dalam metode __del __ ().
Kevin Anderson
3
Pengecualian terlempar jauh sebelum program saya keluar. Pengecualian AttributeError yang saya dapatkan adalah Python mengatakan itu tidak mengenali self.files sebagai atribut dari Paket. Saya mungkin mendapatkan ini salah, tetapi jika dengan "global" mereka tidak berarti variabel global ke metode (tetapi mungkin lokal ke kelas) maka saya tidak tahu apa yang menyebabkan pengecualian ini. Google memberi petunjuk bahwa Python berhak untuk membersihkan data anggota sebelum __del __ (mandiri) dipanggil.
wilhelmtell
1
Kode yang diposkan sepertinya berfungsi untuk saya (dengan Python 2.5). Bisakah Anda memposting kode aktual yang gagal - atau disederhanakan (semakin sederhana versi yang lebih baik yang masih menyebabkan kesalahan?
Silverfish
@ wilhelmtell dapatkah Anda memberikan contoh yang lebih konkret? Dalam semua pengujian saya, del destructor bekerja dengan sempurna.
Tidak diketahui
7
Jika ada yang ingin tahu: Artikel ini menguraikan mengapa __del__tidak boleh digunakan sebagai mitra __init__. (Yaitu, itu bukan "penghancur" dalam arti yang __init__merupakan konstruktor.
franklin

Jawaban:

619

Saya akan merekomendasikan menggunakan withpernyataan Python untuk mengelola sumber daya yang perlu dibersihkan. Masalah dengan menggunakan close()pernyataan eksplisit adalah Anda harus khawatir tentang orang yang lupa menyebutnya sama sekali atau lupa menempatkannya di finallyblok untuk mencegah kebocoran sumber daya ketika pengecualian terjadi.

Untuk menggunakan withpernyataan, buat kelas dengan metode berikut:

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

Dalam contoh Anda di atas, Anda akan menggunakan

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)

Kemudian, ketika seseorang ingin menggunakan kelas Anda, mereka akan melakukan hal berikut:

with Package() as package_obj:
    # use package_obj

Variabel package_obj akan menjadi turunan dari tipe Paket (ini adalah nilai yang dikembalikan oleh __enter__metode). Its __exit__metode secara otomatis akan dipanggil, terlepas dari apakah atau tidak pengecualian terjadi.

Anda bahkan bisa mengambil pendekatan ini selangkah lebih maju. Pada contoh di atas, seseorang masih dapat membuat Instantiate menggunakan konstruktornya tanpa menggunakan withklausa. Anda tidak ingin itu terjadi. Anda bisa memperbaikinya dengan membuat kelas PackageResource yang mendefinisikan __enter__dan __exit__metode. Kemudian, kelas Paket akan didefinisikan secara ketat di dalam __enter__metode dan dikembalikan. Dengan begitu, penelepon tidak pernah dapat membuat instance kelas Paket tanpa menggunakan withpernyataan:

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

Anda akan menggunakan ini sebagai berikut:

with PackageResource() as package_obj:
    # use package_obj
Clint Miller
sumber
35
Secara teknis, seseorang dapat memanggil PackageResource () .__ masukkan __ () secara eksplisit dan dengan demikian membuat Paket yang tidak akan pernah diselesaikan ... tetapi mereka benar-benar harus mencoba untuk memecahkan kode. Mungkin bukan sesuatu yang perlu dikhawatirkan.
David Z
3
Omong-omong, jika Anda menggunakan Python 2.5, Anda harus melakukan dari impor with_statement di masa depan untuk dapat menggunakan pernyataan with.
Clint Miller
2
Saya menemukan artikel yang membantu menunjukkan mengapa __del __ () bertindak seperti itu dan memberikan kepercayaan untuk menggunakan solusi manajer konteks: andy-pearce.com/blog/posts/2013/Apr/python-destructor-drawbacks
eikonomega
2
Bagaimana cara menggunakan konstruk yang bagus dan bersih jika Anda ingin melewatkan parameter? Saya ingin dapat melakukannyawith Resource(param1, param2) as r: # ...
tunda92
4
@ snooze92 Anda bisa memberikan Resource sebuah metode __init__ yang menyimpan * args dan ** kwargs di dalam diri, dan kemudian meneruskannya ke kelas dalam dalam metode enter. Saat menggunakan pernyataan with, __init__ dipanggil sebelum __enter__
Brian Schlenker
48

Cara standar adalah dengan menggunakan 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)

Tetapi Anda harus ingat bahwa ini akan bertahan semua contoh yang dibuat Packagesampai Python dihentikan.

Demo menggunakan kode di atas disimpan sebagai 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
sumber
2
Yang menyenangkan tentang pendekatan atexit.register adalah Anda tidak perlu khawatir tentang apa yang dilakukan pengguna kelas (apakah mereka menggunakan with? Apakah mereka secara eksplisit memanggil __enter__?) Kelemahannya tentu saja jika Anda memerlukan pembersihan untuk terjadi sebelum python keluar, itu tidak akan berhasil. Dalam kasus saya, saya tidak peduli apakah itu ketika objek keluar dari ruang lingkup atau jika tidak sampai python keluar. :)
jauh lagi
Bisakah saya menggunakan enter dan exit dan juga menambahkan atexit.register(self.__exit__)?
myradio
@myradio Saya tidak mengerti bagaimana itu akan berguna? Tidak bisakah Anda melakukan semua logika pembersihan di dalam __exit__, dan menggunakan manajer konteks? Juga, __exit__ambil argumen tambahan (mis. __exit__(self, type, value, traceback)), Jadi Anda perlu mengakses untuk itu. Either way, sepertinya Anda harus memposting pertanyaan terpisah pada SO, karena kasus penggunaan Anda tampak tidak biasa?
ostrokach
33

Sebagai lampiran dari jawaban Clint , Anda dapat menyederhanakan PackageResourcemenggunakan contextlib.contextmanager:

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

Atau, meskipun mungkin bukan sebagai Pythonic, Anda dapat mengesampingkan 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):
        ...

dan cukup gunakan with Package(...) as package.

Untuk mendapatkan yang lebih singkat, beri nama fungsi pembersihan Anda closedan gunakan contextlib.closing, dalam hal ini Anda dapat menggunakan Packagekelas yang tidak dimodifikasi melalui with contextlib.closing(Package(...))atau menimpanya __new__ke yang lebih sederhana

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

Dan konstruktor ini diwarisi, jadi Anda bisa mewarisi, misalnya

class SubPackage(Package):
    def close(self):
        pass
Tobias Kienzler
sumber
1
Ini luar biasa. Saya khususnya menyukai contoh terakhir. Sangat disayangkan bahwa kita tidak dapat menghindari boilerplate empat-line dari Package.__new__()metode, bagaimanapun. Atau mungkin kita bisa. Kita mungkin bisa mendefinisikan dekorator kelas atau metaclass genericizing bahwa boilerplate untuk kita. Makanan untuk pemikiran Pythonic.
Cecil Curry
@CecilCurry Terima kasih, dan poin bagus. Setiap kelas yang mewarisi dari Packagejuga harus melakukan ini (meskipun saya belum mengujinya), jadi tidak diperlukan metaclass. Meskipun saya telah menemukan beberapa cara yang cukup aneh untuk menggunakan metaclasses di masa lalu ...
Tobias Kienzler
@CecilCurry Sebenarnya, konstruktor diwarisi, sehingga Anda dapat menggunakan Package(atau lebih baik nama kelas Closing) sebagai induk kelas Anda, bukan object. Tapi jangan tanya saya berapa banyak warisan yang kacau dengan ini ...
Tobias Kienzler
17

Saya tidak berpikir bahwa mungkin anggota misalnya untuk dihapus sebelum __del__dipanggil. Dugaan saya adalah bahwa alasan untuk AttributeError khusus Anda adalah di tempat lain (mungkin Anda salah menghapus self.file di tempat lain).

Namun, seperti yang ditunjukkan orang lain, Anda harus menghindari penggunaan __del__. Alasan utama untuk ini adalah bahwa instance dengan __del__tidak akan menjadi sampah yang dikumpulkan (mereka hanya akan dibebaskan ketika refcount mereka mencapai 0). Oleh karena itu, jika instance Anda terlibat dalam referensi melingkar, mereka akan hidup dalam memori selama aplikasi berjalan. (Saya mungkin salah tentang semua ini, saya harus membaca gc docs lagi, tapi saya agak yakin ini berfungsi seperti ini).

Dupra Virgil
sumber
5
Objek dengan __del__dapat berupa sampah yang dikumpulkan jika referensi mereka dihitung dari objek lain dengan __del__nol dan tidak dapat dijangkau. Ini berarti jika Anda memiliki siklus referensi antara objek dengan __del__, tidak ada yang akan dikumpulkan. Namun, kasus lain harus diselesaikan seperti yang diharapkan.
Collin
"Dimulai dengan metode Python 3.4, __del __ () tidak lagi mencegah siklus referensi dari pengumpulan sampah, dan modul global tidak lagi dipaksa ke None saat shutdown interpreter. Jadi kode ini harus bekerja tanpa masalah pada CPython." - docs.python.org/3.6/library/…
Tomasz
14

Alternatif yang lebih baik adalah dengan menggunakan lemahref . Finalisasi . Lihat contoh di Finalizer Objects dan Membandingkan finalizer dengan metode __del __ () .

SCGH
sumber
1
Digunakan hari ini dan ini berfungsi dengan sempurna, lebih baik daripada solusi lain. Saya memiliki kelas komunikator berbasis multiprocessing yang membuka port serial dan kemudian saya memiliki stop()metode untuk menutup port dan join()proses. Namun, jika program keluar secara stop()tidak terduga tidak dipanggil - saya menyelesaikannya dengan finalizer. Tetapi bagaimanapun saya memanggil _finalizer.detach()metode stop untuk mencegah memanggilnya dua kali (secara manual dan kemudian lagi oleh finalizer).
Bojan P.
3
IMO, ini benar-benar jawaban terbaik. Ini menggabungkan kemungkinan pembersihan di pengumpulan sampah dengan kemungkinan pembersihan saat keluar. Peringatannya adalah bahwa python 2.7 tidak memiliki kelemahan.
jauh
12

Saya pikir masalahnya bisa di __init__jika ada lebih banyak kode daripada yang ditunjukkan?

__del__akan dipanggil bahkan ketika __init__belum dieksekusi dengan benar atau melemparkan pengecualian.

Sumber

n3o59hf
sumber
2
Kedengarannya sangat mungkin. Cara terbaik untuk menghindari masalah ini ketika menggunakan __del__adalah dengan secara eksplisit mendeklarasikan semua anggota di tingkat kelas, memastikan bahwa mereka selalu ada, bahkan jika __init__gagal. Dalam contoh yang diberikan, files = ()akan berhasil, meskipun sebagian besar Anda hanya akan menetapkan None; dalam kedua kasus, Anda masih perlu menetapkan nilai sebenarnya di __init__.
Søren Løvborg
11

Berikut ini kerangka kerja 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()

Penting: kembalikan diri


Jika Anda seperti saya, dan mengabaikan return selfbagian (dari jawaban Clint Miller yang benar ), Anda akan menatap omong kosong ini:

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

Semoga ini bisa membantu orang selanjutnya.

pengguna2394284
sumber
8

Hanya bungkus destruktor Anda dengan pernyataan try / kecuali dan itu tidak akan membuang pengecualian jika global Anda sudah dibuang.

Edit

Coba ini:

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

Ini akan memasukkan daftar file dalam fungsi del yang dijamin ada pada saat panggilan. Proxy lemahref adalah untuk mencegah Python, atau Anda sendiri menghapus variabel self.files, entah bagaimana (jika dihapus, maka itu tidak akan memengaruhi daftar file asli). Jika bukan karena ini sedang dihapus meskipun ada lebih banyak referensi ke variabel, maka Anda dapat menghapus enkapsulasi proxy.

Tidak dikenal
sumber
2
Masalahnya adalah bahwa jika data anggota hilang, sudah terlambat bagi saya. Saya butuh data itu. Lihat kode saya di atas: Saya perlu nama file untuk mengetahui file mana yang harus dihapus. Saya menyederhanakan kode saya, ada data lain yang perlu saya bersihkan sendiri (yaitu penerjemah tidak akan tahu cara membersihkan).
wilhelmtell
4

Tampaknya cara idiomatis untuk melakukan ini adalah dengan memberikan close()metode (atau yang serupa), dan menyebutnya dengan jelas.

Bastien Léonard
sumber
20
Ini adalah pendekatan yang saya gunakan sebelumnya, tetapi saya mengalami masalah lain dengan itu. Dengan pengecualian yang dilemparkan ke semua tempat oleh perpustakaan lain, saya perlu bantuan Python dalam membersihkan kekacauan jika terjadi kesalahan. Secara khusus, saya perlu Python untuk memanggil destruktor untuk saya, karena kalau tidak, kode menjadi cepat tidak terkelola, dan saya pasti akan melupakan titik keluar di mana panggilan ke .close () seharusnya.
wilhelmtell