淺談__del__()方法的特殊用法
Python中有一些特殊方法,它們允許我們的類和Python更好地集成。在標準庫參考(Standard Library Reference)中,它們被稱為基本特殊方法,是與Python的其他特性無縫集成的基礎。
__del__()方法有一個讓人費解的使用場景。
這個方法的目的是在將一個對象從內存中清除之前,可以有機會做一些清理工作。如果使用上下文管理對象或者with語句來處理這種需求會更加清晰。對于Python的垃圾回收機制而言,創建一個上下文比使用__del__()更加容易預判。
但是,如果一個Python對象包含了一些操作系統的資源,__del__()方法是把資源從程序中釋放的最后機會。例如,引用了一個打開的文件、安裝好的設備或者子進程的對象,如果我們將資源釋放作為__del__()方法的一部分實現,那么我們就可以保證這些資源最后會被釋放。
很難預測什么時候__del__()方法會被調用。它并不總是在使用del語句刪除對象時被調用,當一個對象因為命名空間被移除而被刪除時,它也不一定被調用。Python文檔中用不穩定來描述__del__()方法的這種行為,并且提供了額外的關于異常處理的注釋:運行期的異常會被忽略,相對地,會使用sys.stderr打印一個警告。
基于上面的這些原因,通常更傾向于使用上下文管理器,而不是實現__del__()。
1. 引用計數和對象銷毀
CPython的實現中,對象會包括一個引用計數器。當對象被賦值給一個變量時,這個計數器會遞增;當變量被刪除時,這個計數器會遞減。當引用計數器的值為0時,表示我們的程序不再需要這個對象,并且可以銷毀這個對象。對于簡單對象,當執行刪除對象的操作時會調用__del__()方法。
對于包含循環引用的復雜對象,引用計數器有可能永遠也不會歸零,這樣就很難讓__del__()方法被調用。
我們用下面的一個類來看看這個過程中到底發生了什么。
class?Noisy:? def?__del__(?self?):? print(?"Removing?{0}".format(id(self))?)
我們可以像下面這樣創建和刪除這個對象。
>>>?x=?Noisy()?>>>del?x?Removing?4313946640
我們先創建,然后刪除了Noisy對象,幾乎是立刻就看到了__del__()方法中輸出的消息。這也就是說,當變量x被刪除后,引用計數器正確地歸零了。一旦變量被刪除,就沒有任何地方引用Noisy實例,所以它也可以被清除。
下面是淺復制中一種常見的情形。
>>>?ln?=?[?Noisy(),?Noisy()?]?>>>?ln2=?ln[:]?>>>?del?ln
Python沒有響應del語句。這說明這些Noisy對象的引用計數器還沒有歸零,肯定還有其他地方引用了它們,下面的代碼驗證了這一點。
>>>?del?ln2?Removing?4313920336?Removing?4313920208
ln2變量是ln列表的一個淺復制。有兩個列表引用了Noisy對象,所以在這兩個列表被刪除并且引用計數器歸零之前,Python不會銷毀這兩個Noisy對象。
還有很多種創建淺復制的方法。下面是其中的一些。
a?=?b?=?Noisy()?c?=?[?Noisy()?]?*?2
這里的關鍵是,由于淺復制在Python中非常普遍,所以我們往往對存在的對象的引用感到非常困惑。
2. 循環引用和垃圾回收
下面是一種常見的循環引用的情形。一個父類包含一個子類的集合,同時集合中的每個子類實例又包含父類的引用。
下面我們用這兩個類來看看循環引用。
class?Parent:? def?__init__(?self,?*children?):? self.children=?list(children)? for?child?in?self.children:? child.parent=?self? def?__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))?)
一個Parent的instance包括一個children的列表。
每一個Child的實例都有一個指向Parent類的引用。當向Parent內部的集合中插入新的Child實例時,這個引用就會被創建。
我們故意把這兩個類寫得比較復雜,所以下面讓我們看看當試圖刪除對象時,會發生什么。
>>>>?p?=?Parent(?Child(),?Child()?)?>>>?id(p)?4313921808?>>>?del?p
Parent和它的兩個初始Child實例都不能被刪除,因為它們之間互相引用。
下面,我們創建一個沒有Child集合的Parent實例。
>>>?p=?Parent()?>>>?id(p)?4313921744?>>>?del?p?Removing?Parent?4313921744
和我們預期的一樣,這個Parent實例成功地被刪除了。
由于互相之間有引用存在,因此我們不能從內存中刪除Parent實例和它包含的Child實例的集合。如果我們導入垃圾回收器的接口——gc,我們就可以回收和顯示這些不能被刪除的對象。
下面的代碼中,我們使用了gc.collect()方法回收所有定義了__del__()方法但是無法被刪除的對象。
>>>?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對象(例如,4313921808的ID = 0x101213910)在不可刪除的垃圾對象列表中很突出。為了讓引用計數器歸零,我們需要刪除所有Parent對象中的children列表,或者刪除所有Child實例中對Parent的引用。
注意,即使把清理資源的代碼放在__del__()方法中,我們也沒辦法解決循環引用的問題。因為__del__()方法是在循環引用被解除并且引用計數器已經歸零之后被調用的。當有循環引用時,我們不能只是簡單地依賴于Python中計算引用數量的機制來清理內存中的無用對象。我們必須顯式地解除循環引用或者使用可以保證垃圾回收的weakref引用。
3. 循環引用和weakref模塊
如果我們需要循環引用,但是又希望將清理資源的代碼寫在__del__()中,這時候我們可以使用弱引用。循環引用的一個常見場景是互相引用:一個父類中包含了一個集合,集合中的每一個實例也包含了一個指向父類的引用。如果一個Player對象中包含多個Hand實例,那么在每一個Hand對象中都包括一個指向對應的Player類的引用可能會更方便。
默認的對象間的引用可以被稱為強引用,但是,叫直接引用可能更好。Python的引用計數機制會直接使用它們,而且如果引用計數無法刪除這些對象的話,垃圾回收機器也能及時發現。它們是不可忽略的對象。
對一個對象的強引用就是直接引用,下面是一個例子。
當我們遇到如下語句。
a=?B()
變量a直接引用了B類的一個對象。此時B的引用計數至少是1,因為a變量包含了一個指向它的引用。
想要找個一個弱引用相關的對象需要兩個步驟。一個弱引用會調用x.parent(),這個函數將弱引用作為一個可調用對象來查找它真正的父對象。這個過程讓引用計數器得以歸零,垃圾回收器可以回收引用的對象,但是不回收這個弱引用。
weakref定義了一系列使用了弱引用而沒有使用強引用的集合。它讓我們可以創建一種特殊的字典類型,當這種字典的對象沒有用時,可以保證被垃圾回收。
我們可以修改Parent和Child類,在Child指向Parent的引用中使用弱引用,這樣就可以簡單地保證無用對象會被銷毀。
下面是修改后的類,它在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引用改為一個weakref對象的引用。
在Child類中,我們必須用上面說的兩步操作來定位parent對象。
p?=?self.parent()?if?p?is?not?None:? #?process?p,?the?Parent?instance?else:? #?the?parent?instance?was?garbage?collected.
我們可以顯式地確認引用的對象是否已經找到,因為有可能該引用已經變成虛引用。
當我們使用這個新的Parent2類時,可以看到引用計數成功地歸零同時對象也被刪除了。
>>>?p?=?Parent2(?Child(),?Child()?)?>>>?del?p?Removing?Parent2?4303253584?Removing?Child?4303256464?Removing?Child?4303043344
當一個weakref引用變成死引用時(因為引用被銷毀了),我們有3個可能的方案。
重新創建引用對象,或重新從數據庫中加載。
當垃圾回收器在低內存情況下錯誤地刪除了一些對象時,使用warnings模塊記錄調試信息。
忽略這個問題。
通常,weakref引用變成死引用是因為響應的對象已經被刪除了。例如,變量的作用域已經執行結束,一個沒有用的命名空間,應用程序正在關閉。對于這個原因,通常我們會采取第3種響應方法。因為試圖創建這個引用的對象時很可能馬上就會被刪除。
4. __del__()方法和close()方法
__del__()方法最常見的用途是確保文件被關閉。
通常,包含文件操作的類都會有類似下面這樣的代碼。
__del__?=?close
這會保證__del__()方法同時也是close()方法。
其他更復雜的情況最好使用上下文管理器。
以上內容節選自《Python面向對象編程指南》
Steven F. Lott的編程生涯開始于20世紀70年代,那時候計算機體積很大、昂貴并且非常少見。作為軟件工程師和架構師,他參與了100多個不同規模的項目研發。在使用Python解決業務問題方面,他已經有10多年的經驗了。
Steven目前是自由職業者,居住在美國東海岸。他的技術博客是:http://slott-softwarearchitect.blogspot.com。
譯者:
張心韜——新加坡國立大學系統分析碩士,北京航空航天大學北海學院軟件工程學士。曾經就職于NEC(新加坡)和MobileOne(新加坡),目前投身金融領域,就職于GoSwiff(新加坡),擔任.NET軟件工程師,負責支付系統的研發工作。
他在編程領域耕耘數年,涉獵甚廣,但自認“既非菜鳥,也非高人”。目前長期專注于.NET平臺,對Python也甚為喜愛。業余時間愛好甚廣,尤其喜歡學習中醫知識,對時間管理、經濟和歷史也略有涉獵。
蘭亮:北京航空航天大學北海學院軟件工程學士,IT行業一線“碼農”,曾獲評“微軟2014年度MVP”和“微軟2015年度MVP”。曾一度混跡于飛信(中國)、NEC(新加坡)和MobileOne(新加坡),現就職于Keritos(新加坡),從事在線游戲研發工作。
他雖然涉獵廣泛,但鐘愛開源,長期關注前沿技術,并且對算法、函數式編程、設計模式以及IT文化等有著濃厚興趣。工作之余,他喜歡在Coursera蹭課。作為一個熱愛生活的人,他在鉆研技術之余,還喜歡健身、旅行,立志成為一個陽光、向上的“碼農”。
蘭亮個人網站 :?www.lan-liang.me
博客:http://blog.csdn.net/lan_liang
本文轉載自異步社區
軟件開發 編程語言 python
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。