Bir Python nesnesini nasıl doğru bir şekilde temizlerim?


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

    # ...

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

__del__(self)Yukarıdaki bir AttributeError istisnası ile başarısız olur. Python__del__() çağrıldığında "küresel değişkenler" (bu bağlamda üye verileri?) Varlığını garanti etmediğini biliyorum . Durum böyleyse ve istisnanın nedeni buysa, nesnenin düzgün şekilde yok edildiğinden nasıl emin olabilirim?


3
Ne bağlandığınızı okumak, global değişkenler gidiyor programdan çıkarken hakkında konuşmak sürece burada geçerli görünmüyor, bu sırada ne bağladığınıza göre os modülünün kendisi zaten OLMUŞ olabilir. Aksi takdirde, bir __del __ () yöntemindeki üye değişkenler için geçerli olduğunu sanmıyorum.
Kevin Anderson

3
Programımın çıkmasından çok önce istisna atılır. Aldığım AttributeError istisnası, Python'un self.files'i Paket'in bir özniteliği olarak tanımadığını söylüyor. Bunu yanlış anlıyor olabilirim, ama eğer "globaller" tarafından yöntemlere küresel değişkenler (ama muhtemelen sınıf için yerel) anlamına gelmezse, o zaman bu istisna neden ne bilmiyorum. Google, Python'un __del __ (self) çağrılmadan önce üye verilerini temizleme hakkını saklı tutar.
wilhelmtell

1
Yayınlandığı gibi kod benim için çalışıyor gibi görünüyor (Python 2.5 ile). Başarısız olan gerçek kodu (ya da basitleştirilmiş) (hala hataya neden olan daha basit daha iyi sürüm) gönderebilir misiniz?
Silverfish,

@ wilhelmtell daha somut bir örnek verebilir misiniz? Tüm testlerimde del destructor mükemmel çalışıyor.
Bilinmiyor

7
Biri bilmek isterse: Bu makale neden __del__muadili olarak kullanılmaması gerektiğini açıklamaktadır __init__. (Yani, __init__bir kurucu anlamında bir "yıkıcı" değildir .
franklin

Yanıtlar:


619

withTemizlenmesi gereken kaynakları yönetmek için Python'un ifadesini kullanmanızı tavsiye ederim . Açık bir close()ifadeyi kullanmayla ilgili sorun, finallyherhangi bir istisna olduğunda kaynak sızıntısını önlemek için onu hiç çağırmayı veya bir bloğa yerleştirmeyi unutmayı düşünen kişilerdir .

İfadeyi kullanmak için withaşağıdaki yöntemlerle bir sınıf oluşturun:

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

Yukarıdaki örnekte şunu kullanırsınız:

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)

Sonra, birisi sınıfınızı kullanmak istediğinde aşağıdakileri yapar:

with Package() as package_obj:
    # use package_obj

Package_obj değişkeni Package türünün bir örneği olacaktır ( __enter__yöntem tarafından döndürülen değerdir ). Onun __exit__yöntemi bağımsız olarak otomatik bir özel durum oluşur olup olmadığına, çağrılır.

Hatta bu yaklaşımı bir adım daha ileri götürebilirsiniz. Yukarıdaki örnekte, birisi yine de withmaddeyi kullanmadan paketini yapıcısını kullanarak başlatabilir . Bunun olmasını istemiyorsun. __enter__Ve __exit__yöntemlerini tanımlayan bir PackageResource sınıfı oluşturarak bunu düzeltebilirsiniz . Daha sonra Package sınıfı kesinlikle __enter__yöntemin içinde tanımlanır ve döndürülür. Bu şekilde, arayan hiçbir zaman bir paket kullanmadan Package sınıfını başlatamaz 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()

Bunu aşağıdaki gibi kullanırsınız:

with PackageResource() as package_obj:
    # use package_obj

35
Teknik olarak konuşursak, PackageResource () .__ gir __ () yöntemini açıkça arayabilir ve böylece asla sonlandırılmayacak bir Paket oluşturabilir ... ama gerçekten kodu kırmaya çalışıyorlardı. Muhtemelen endişelenecek bir şey değil.
David Z

3
Bu arada, Python 2.5 kullanıyorsanız, with ifadesini kullanabilmek için gelecekteki içe aktarma ile_statement yapmalısınız.
Clint Miller

2
__Del __ () işlevinin neden böyle davrandığını göstermeye yardımcı olan ve bir bağlam yöneticisi çözümü kullanmaya güvenilen bir makale buldum: andy-pearce.com/blog/posts/2013/Apr/python-destructor-drawbacks
eikonomega

2
Parametreleri geçmek istiyorsanız bu güzel ve temiz yapıyı nasıl kullanabilirsiniz? with Resource(param1, param2) as r: # ...
Yapabilmek

4
@ snooze92, Resource'a * args ve ** kwarg'ları kendi kendine saklayan ve sonra enter yönteminde iç sınıfa aktaran bir __init__ yöntemi verebilirsiniz. With ifadesini kullanırken __init__, __enter__'den önce çağrılır
Brian Schlenker

48

Standart yol kullanmaktır 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)

Ancak, bunun PackagePython sonlandırılana kadar oluşturulan tüm örneklerin devam edeceğini unutmayın .

Package.py olarak kaydedilen yukarıdaki kodu kullanarak demo yapın :

$ 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...

2
Atexit.register yaklaşımı hakkında güzel bir şey, sınıftaki kullanıcının ne yaptığı hakkında endişelenmenize gerek olmamasıdır (kullandılar withmı? Açıkça çağırdılar mı __enter__?) çıkar, işe yaramaz. Benim durumumda, nesnenin kapsam dışına çıktığı zaman mı yoksa python çıkana kadar mı olduğu umurumda değil. :)
hlongmore

Enter ve exit komutlarını kullanabilir ve ekleyebilir atexit.register(self.__exit__)miyim?
myradio

@ myradio Bunun nasıl faydalı olacağını görmüyorum? Tüm temizleme mantığını içeride gerçekleştiremiyor __exit__ve bir bağlam yöneticisi kullanamıyor musunuz? Ayrıca, __exit__ek argümanlar (yani __exit__(self, type, value, traceback)) alır, bu yüzden bunlar için hesap vermeniz gerekir. Her iki durumda da, SO'ya ayrı bir soru göndermeniz gerektiği anlaşılıyor, çünkü kullanım durumunuz olağandışı görünüyor?
ostrokach

33

Clint'in cevabının bir eki olarak , aşağıdakileri PackageResourcekullanarak basitleştirebilirsiniz contextlib.contextmanager:

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

Alternatif olarak, muhtemelen Pythonic olarak olmasa da, geçersiz kılabilirsiniz 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):
        ...

ve sadece kullanın with Package(...) as package.

İşleri kısaltmak için temizleme işlevinizi adlandırın closeve kullanın contextlib.closing; bu durumda değiştirilmemiş Packagesınıfı kullanarak with contextlib.closing(Package(...))veya __new__daha basit olanı geçersiz kılabilirsiniz

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

Ve bu yapıcı kalıtsaldır, böylece miras alabilirsiniz

class SubPackage(Package):
    def close(self):
        pass

1
Bu harika. Özellikle son örneği seviyorum. Package.__new__()Bununla birlikte , yöntemin dört hatlı kaynatma plakasından kaçınamayız . Ya da belki yapabiliriz. Muhtemelen bir sınıf dekoratörü veya bu ortak plakayı bizim için jenerikleştiren bir metasınıf tanımlayabiliriz. Pitonik düşünce için yiyecek.
Cecil Curry

@CecilCurry Teşekkürler ve iyi bir nokta. Gelen herhangi bir sınıf Packageda bunu (henüz test etmedim rağmen) bunu yapmak gerekir, bu yüzden metasınıf gerekmez. Ben rağmen gelmiş geçmiş metaclasses kullanmak için bazı oldukça meraklı yollar ... bulundu
Tobias Kienzler

@CecilCurry Aslında, yapıcı devralınır, bu nedenle sınıf üst öğeniz yerine sınıf üst öğesi olarak kullanabilirsiniz Package(veya daha iyi bir sınıf kullanabilirsiniz Closing) object. Ama bana bununla birden fazla mirasın nasıl
bozulduğunu sorma

17

Örnek üyelerin daha önce kaldırılmasının mümkün olduğunu düşünmüyorum __del__. Benim tahminim, sizin AttributeError özelliğinin başka bir yerde olması olabilir (belki yanlışlıkla selffile'ı başka bir yerde kaldırırsınız).

Ancak, diğerlerinin de belirttiği gibi, kullanmaktan kaçınmalısınız __del__. Bunun temel nedeni, __del__çöp içeren vakaların toplanmamasıdır (yalnızca refcountları 0'a ulaştığında serbest bırakılacaktır). Bu nedenle, örnekleriniz döngüsel başvurularla ilgiliyse, uygulama çalıştığı sürece bellekte yaşarlar. (Tüm bunlar hakkında yanılıyor olabilir, yine gc belgelerini tekrar okumak zorundayım, ama bunun böyle çalıştığından eminim).


5
İle olan __del__nesneler, diğer nesnelerden referans sayıları __del__sıfırsa ve ulaşılamazsa çöp toplanabilir . Bu, ile nesneler arasında bir referans döngünüz varsa __del__, bunların hiçbirinin toplanmayacağı anlamına gelir. Ancak, diğer tüm durumlar beklendiği gibi çözülmelidir.
Collin

"Python 3.4 ile başlayarak, __del __ () yöntemleri artık referans döngülerinin çöp toplanmasını engellemez ve yorumlayıcı kapanması sırasında modül globalleri artık Yok'a zorlanmaz. Bu nedenle bu kod CPython ile ilgili herhangi bir sorun olmadan çalışmalıdır." - docs.python.org/3.6/library/…
Tomasz Gandor

14

Daha iyi bir alternatif, zayıfref.finalize kullanmaktır . Sonlandırıcı Nesneleri ve Sonlandırıcıları __del __ () yöntemleriyle karşılaştırma konusundaki örneklere bakın .


1
Bunu bugün kullandık ve diğer çözümlerden daha iyi, kusursuz çalışıyor. Bir seri bağlantı noktası açan çok işlemcili tabanlı iletişimci sınıfı var ve sonra stop()bağlantı noktalarını ve join()işlemleri kapatmak için bir yöntemim var . Ancak, programlar beklenmedik şekilde çıkarsa stop()çağrılmaz - bunu bir sonlandırıcıyla çözdüm. Ama her durumda _finalizer.detach()iki kez (sonlandırıcı tarafından manuel ve daha sonra) çağrılmasını önlemek için stop yöntemini çağırıyorum.
Bojan P.

3
IMO, bu gerçekten en iyi cevap. Çöp toplamada temizleme olasılığını çıkışta temizleme imkanı ile birleştirir. Uyarı, python 2.7'nin zayıf bir ifadeye sahip olmamasıdır.
süre

12

__init__Gösterilenden daha fazla kod varsa sorun olabilir düşünüyorum ?

__del____init__düzgün yürütülmediğinde veya bir istisna attığında bile çağrılır .

Kaynak


2
Çok muhtemel görünüyor. Kullanırken bu sorunu önlemenin en iyi yolu, __del__tüm üyeleri sınıf düzeyinde açıkça bildirmek ve __init__başarısız olsalar bile her zaman var olmalarını sağlamaktır . Verilen örnekte, files = ()çoğunlukla atayacağınız halde işe yarar None; her iki durumda da, içindeki gerçek değeri atamanız gerekir __init__.
Søren Løvborg

11

İşte minimal bir çalışma iskeleti:

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

Önemli: kendini iade et


Eğer benim gibiyseniz ve return self( Clint Miller'ın doğru cevabının ) kısmına bakmazsanız, bu saçmalıklara bakacaksınız:

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

Umarım bir sonraki kişiye yardımcı olur.


8

Sadece yıkımcınızı bir dene / hariç ifadesiyle sarın ve eğer küreselleriniz zaten imha edilmişse bir istisna atmayacaktır.

Düzenle

Bunu dene:

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

Arama sırasında var olması garanti edilen del fonksiyonundaki dosya listesini doldurur. Zayıfref proxy'si, Python'un veya kendinizin self.files değişkenini bir şekilde silmesini önlemektir (silinirse, orijinal dosya listesini etkilemez). Değişken için daha fazla referans olmasına rağmen bunun silinmesi söz konusu değilse, proxy kapsülleme özelliğini kaldırabilirsiniz.


2
Sorun şu ki, üye verileri kaybolduysa benim için çok geç. Bu verilere ihtiyacım var. Yukarıdaki koduma bakın: Kaldırılacak dosyaları bilmek için dosya adlarına ihtiyacım var. Kodumu basitleştirdim, kendimi temizlemem gereken başka veriler var (yani yorumlayıcı nasıl temizleneceğini bilemez).
wilhelmtell

4

Öyle görünüyor ki, bunu yapmanın deyimsel yolu bir close()yöntem (veya benzeri) sağlamak ve bunu açıkça çağırmaktır.


20
Daha önce kullandığım yaklaşım bu, ama onunla başka problemlerle karşılaştım. Diğer kütüphaneler tarafından her yere atılan istisnalar dışında, bir hata durumunda karışıklığı temizlemek için Python'un yardımına ihtiyacım var. Özellikle, benim için yıkıcı çağırmak için Python gerekir, aksi takdirde kod hızla yönetilemez hale gelir ve ben kesinlikle .close () çağrısı olması gereken bir çıkış noktasını unutacağım.
wilhelmtell
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.