《Python面向对象编程指南》——2.7 __del__()方法
本節(jié)書摘來自異步社區(qū)《Python面向?qū)ο缶幊讨改稀芬粫械牡?章,第2.7節(jié),作者[美]Steven F. Lott, 張心韜 蘭亮 譯,更多章節(jié)內(nèi)容可以訪問云棲社區(qū)“異步社區(qū)”公眾號(hào)查看。
2.7 __del__()方法
__del__()方法有一個(gè)讓人費(fèi)解的使用場(chǎng)景。
這個(gè)方法的目的是在將一個(gè)對(duì)象從內(nèi)存中清除之前,可以有機(jī)會(huì)做一些清理工作。如果使用上下文管理對(duì)象或者with語句來處理這種需求會(huì)更加清晰,這也是第5章“可調(diào)用對(duì)象和上下文的使用”的內(nèi)容。對(duì)于Python的垃圾回收機(jī)制而言,創(chuàng)建一個(gè)上下文比使用__del__()更加容易預(yù)判。
但是,如果一個(gè)Python對(duì)象包含了一些操作系統(tǒng)的資源,__del__()方法是把資源從程序中釋放的最后機(jī)會(huì)。例如,引用了一個(gè)打開的文件、安裝好的設(shè)備或者子進(jìn)程的對(duì)象,如果我們將資源釋放作為__del__()方法的一部分實(shí)現(xiàn),那么我們就可以保證這些資源最后會(huì)被釋放。
很難預(yù)測(cè)什么時(shí)候__del__()方法會(huì)被調(diào)用。它并不總是在使用del語句刪除對(duì)象時(shí)被調(diào)用,當(dāng)一個(gè)對(duì)象因?yàn)槊臻g被移除而被刪除時(shí),它也不一定被調(diào)用。Python文檔中用不穩(wěn)定來描述__del__()方法的這種行為,并且提供了額外的關(guān)于異常處理的注釋:運(yùn)行期的異常會(huì)被忽略,相對(duì)地,會(huì)使用sys.stderr打印一個(gè)警告。
基于上面的這些原因,通常更傾向于使用上下文管理器,而不是實(shí)現(xiàn)__del__()。
2.7.1 引用計(jì)數(shù)和對(duì)象銷毀
CPython的實(shí)現(xiàn)中,對(duì)象會(huì)包括一個(gè)引用計(jì)數(shù)器。當(dāng)對(duì)象被賦值給一個(gè)變量時(shí),這個(gè)計(jì)數(shù)器會(huì)遞增;當(dāng)變量被刪除時(shí),這個(gè)計(jì)數(shù)器會(huì)遞減。當(dāng)引用計(jì)數(shù)器的值為0時(shí),表示我們的程序不再需要這個(gè)對(duì)象并且可以銷毀這個(gè)對(duì)象。對(duì)于簡(jiǎn)單對(duì)象,當(dāng)執(zhí)行刪除對(duì)象的操作時(shí)會(huì)調(diào)用__del__()方法。
對(duì)于包含循環(huán)引用的復(fù)雜對(duì)象,引用計(jì)數(shù)器有可能永遠(yuǎn)也不會(huì)歸零,這樣就很難讓__del__()被調(diào)用。
我們用下面的一個(gè)類來看看這個(gè)過程中到底發(fā)生了什么。
class Noisy:def __del__( self ):print( "Removing {0}".format(id(self)) )我們可以像下面這樣創(chuàng)建和刪除這個(gè)對(duì)象。
>>> x= Noisy() >>>del x Removing 4313946640我們先創(chuàng)建,然后刪除了Noisy對(duì)象,幾乎是立刻就看到了__del__()方法中輸出的消息。這也就是說當(dāng)變量x被刪除后,引用計(jì)數(shù)器正確地歸零了。一旦變量被刪除,就沒有任何地方引用Noisy實(shí)例,所以它也可以被清除。
下面是淺復(fù)制中一種常見的情形。
>>> ln = [ Noisy(), Noisy() ] >>> ln2= ln[:] >>> del lnPython沒有響應(yīng)del語句。這說明這些Noisy對(duì)象的引用計(jì)數(shù)器還沒有歸零,肯定還有其他地方引用了它們,下面的代碼驗(yàn)證了這一點(diǎn)。
>>> del ln2 Removing 4313920336 Removing 4313920208ln2變量是ln列表的一個(gè)淺復(fù)制。有兩個(gè)列表引用了Noisy對(duì)象,所以在這兩個(gè)列表被刪除并且引用計(jì)數(shù)器歸零之前,Python不會(huì)銷毀這兩個(gè)Noisy對(duì)象。
還有很多種創(chuàng)建淺復(fù)制的方法。下面是其中的一些。
a = b = Noisy() c = [ Noisy() ] * 2這里的關(guān)鍵是,由于淺復(fù)制在Python中非常普遍,所以我們往往對(duì)存在的對(duì)象的引用感到非常困惑。
2.7.2 循環(huán)引用和垃圾回收
下面是一種常見的循環(huán)引用的情形。一個(gè)父類包含一個(gè)子類的集合,同時(shí)集合中的每個(gè)子類實(shí)例又包含父類的引用。
下面我們用這兩個(gè)類來看看循環(huán)引用。
class Parent:def __init__( self, *children ):self.children= list(children)for child in self.children:child.parent= selfdef __del__( self ):print( "Removing {__class__.__name__} {id:d}". format( __class__=self.__class__, id=id(self)) ) class Child:def __del__( self ):print( "Removing {__class__.__name__} {id:d}". format( __class__=self.__class__, id=id(self)) )一個(gè)Parent的instance包括一個(gè)children的列表。
每一個(gè)Child的實(shí)例都有一個(gè)指向Parent類的引用。當(dāng)向Parent內(nèi)部的集合中插入新的Child實(shí)例時(shí),這個(gè)引用就會(huì)被創(chuàng)建。
我們故意把這兩個(gè)類寫得比較復(fù)雜,所以下面讓我們看看當(dāng)試圖刪除對(duì)象時(shí),會(huì)發(fā)生什么。
>>>> p = Parent( Child(), Child() ) >>> id(p) 4313921808 >>> del pParent和它的兩個(gè)初始Child實(shí)例都不能被刪除,因?yàn)樗鼈冎g互相引用。
下面,我們創(chuàng)建一個(gè)沒有Child集合的Parent實(shí)例。
>>> p= Parent() >>> id(p) 4313921744 >>> del p Removing Parent 4313921744和我們預(yù)期的一樣,這個(gè)Parent實(shí)例成功地被刪除了。
由于互相之間有引用存在,因此我們不能從內(nèi)存中刪除Parent實(shí)例和它包含的Child實(shí)例的集合。如果我們導(dǎo)入垃圾回收器的接口——gc,我們就可以回收和顯示這些不能被刪除的對(duì)象。
下面的代碼中,我們使用了gc.collect()方法回收所有定義了__del__()方法但是無法被刪除的對(duì)象。
>>> import gc >>> gc.collect() 174 >>> gc.garbage [<__main__.Parent object at 0x101213910>, <__main__.Child object at 0x101213890>, <__main__.Child object at 0x101213650>, <__main__.Parent object at 0x101213850>, <__main__.Child object at 0x1012130d0>, <__main__.Child object at 0x101219a10>, <__main__.Parent object at 0x101213250>, <__main__.Child object at 0x101213090>, <__main__.Child object at 0x101219810>, <__main__.Parent object at 0x101213050>, <__main__.Child object at 0x101213210>, <__main__.Child object at 0x101219f90>, <__main__.Parent object at 0x101213810>, <__main__.Child object at 0x1012137d0>, <__main__.Child object at 0x101213790>]可以看到,我們的Parent對(duì)象(例如,4313921808的ID = 0x101213910)在不可刪除的垃圾對(duì)象列表中很突出。為了讓引用計(jì)數(shù)器歸零,我們需要?jiǎng)h除所有Parent對(duì)象中的children列表,或者刪除所有Child實(shí)例中對(duì)Parent的引用。
注意,即使把清理資源的代碼放在__del__()方法中,我們也沒辦法解決循環(huán)引用的問題。因?yàn)開_del__()方法是在循環(huán)引用被解除并且引用計(jì)數(shù)器已經(jīng)歸零之后被調(diào)用的。當(dāng)有循環(huán)引用時(shí),我們不能只是簡(jiǎn)單地依賴于Python中計(jì)算引用數(shù)量的機(jī)制來清理內(nèi)存中的無用對(duì)象。我們必須顯式地解除循環(huán)引用或者使用可以保證垃圾回收的weakref引用。
2.7.3 循環(huán)引用和weakref模塊
如果我們需要循環(huán)引用,但是又希望將清理資源的代碼寫在__del__()中,這時(shí)候我們可以使用弱引用。循環(huán)引用的一個(gè)常見場(chǎng)景是互相引用:一個(gè)父類中包含了一個(gè)集合,集合中的每一個(gè)實(shí)例也包含了一個(gè)指向父類的引用。如果一個(gè)Player對(duì)象中包含多個(gè)Hand實(shí)例,那么在每一個(gè)Hand對(duì)象中都包括一個(gè)指向?qū)?yīng)的Player類的引用可能會(huì)更方便。
默認(rèn)的對(duì)象間的引用可以被稱為強(qiáng)引用,但是,叫直接引用可能更好。Python的引用計(jì)數(shù)機(jī)制會(huì)直接使用它們,而且如果引用計(jì)數(shù)無法刪除這些對(duì)象的話,垃圾回收機(jī)器也能及時(shí)發(fā)現(xiàn)。它們是不可忽略的對(duì)象。
對(duì)一個(gè)對(duì)象的強(qiáng)引用就是直接引用,下面是一個(gè)例子。
當(dāng)我們遇到如下語句。
a= B()
變量a直接引用了B類的一個(gè)對(duì)象。此時(shí)B的引用計(jì)數(shù)至少是1,因?yàn)閍變量包含了一個(gè)指向它的引用。
想要找個(gè)一個(gè)弱引用相關(guān)的對(duì)象需要兩個(gè)步驟。一個(gè)弱引用會(huì)調(diào)用x.parent(),這個(gè)函數(shù)將弱引用作為一個(gè)可調(diào)用對(duì)象來查找它真正的父對(duì)象。這個(gè)過程讓引用計(jì)數(shù)器得以歸零,垃圾回收器可以回收引用的對(duì)象,但是不回收這個(gè)弱引用。
weakref定義了一系列使用了弱引用而沒有使用強(qiáng)引用的集合。它讓我們可以創(chuàng)建一種特殊的字典類型,當(dāng)這種字典的對(duì)象沒有用時(shí),可以保證被垃圾回收。
我們可以修改Parent和Child類,在Child指向Parent的引用中使用弱引用,這樣就可以簡(jiǎn)單地保證無用對(duì)象會(huì)被銷毀。
下面是修改后的類,它在Child指向Parent的引用中使用了弱引用。
import weakref class Parent2:def __init__( self, *children ):self.children= list(children)for child in self.children:child.parent= weakref.ref(self)def __del__( self ):print( "Removing {__class__.__name__} {id:d}".format( __class__= self.__class__, id=id(self)) )我們將child中的parent引用改為一個(gè)weakref對(duì)象的引用。
在Child類中,我們必須用上面說的兩步操作來定位parent對(duì)象:
p = self.parent() if p is not None:# process p, the Parent instance else:# the parent instance was garbage collected.我們可以顯式地確認(rèn)引用的對(duì)象是否已經(jīng)找到,因?yàn)橛锌赡茉撘靡呀?jīng)變成虛引用。
當(dāng)我們使用這個(gè)新的Parent2類時(shí),可以看到引用計(jì)數(shù)成功地歸零同時(shí)對(duì)象也被刪除了:
>>> p = Parent2( Child(), Child() ) >>> del p Removing Parent2 4303253584 Removing Child 4303256464 Removing Child 4303043344當(dāng)一個(gè)weakref引用變成死引用時(shí)(因?yàn)橐帽讳N毀了),我們有3個(gè)可能的方案。
- 重新創(chuàng)建引用對(duì)象,或重新從數(shù)據(jù)庫中加載。
- 當(dāng)垃圾回收器在低內(nèi)存情況下錯(cuò)誤地刪除了一些對(duì)象時(shí),使用warnings模塊記錄調(diào)試信息。
- 忽略這個(gè)問題。
通常,weakref引用變成死引用是因?yàn)轫憫?yīng)的對(duì)象已經(jīng)被刪除了。例如,變量的作用域已經(jīng)執(zhí)行結(jié)束,一個(gè)沒有用的命名空間,應(yīng)用程序正在關(guān)閉。對(duì)于這個(gè)原因,通常我們會(huì)采取第3種響應(yīng)方法。因?yàn)樵噲D創(chuàng)建這個(gè)引用的對(duì)象時(shí)很可能馬上就會(huì)被刪除。
2.7.4 __del__()和close()方法
__del__()最常見的用途是確保文件被關(guān)閉。
通常,包含文件操作的類都會(huì)有類似下面這樣的代碼。
__del__ = close
這會(huì)保證__del__()方法同時(shí)也是close()方法。
其他更復(fù)雜的情況最好使用上下文管理器。詳情請(qǐng)看第5章“可調(diào)用對(duì)象和上下文的使用”,我們會(huì)在第5章提供更多和上下文管理器有關(guān)的信息。
總結(jié)
以上是生活随笔為你收集整理的《Python面向对象编程指南》——2.7 __del__()方法的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 时间轮
- 下一篇: websocket python爬虫_p