本節書摘來自異步社區《Python面向對象編程指南》一書中的第2章,第2.7節,作者[美]Steven F. Lott, 張心韜 蘭亮 譯,更多章節內容可以訪問云棲社區“異步社區”公眾號查看。
2.7 __del__()方法
__del__()
方法有一個讓人費解的使用場景。
這個方法的目的是在將一個對象從內存中清除之前,可以有機會做一些清理工作。如果使用上下文管理對象或者with語句來處理這種需求會更加清晰,這也是第5章“可調用對象和上下文的使用”的內容。對于Python的垃圾回收機制而言,創建一個上下文比使用__del__()更加容易預判。
但是,如果一個Python對象包含了一些操作系統的資源,__del__()方法是把資源從程序中釋放的最后機會。例如,引用了一個打開的文件、安裝好的設備或者子進程的對象,如果我們將資源釋放作為__del__()方法的一部分實現,那么我們就可以保證這些資源最后會被釋放。
很難預測什么時候__del__()方法會被調用。它并不總是在使用del語句刪除對象時被調用,當一個對象因為命名空間被移除而被刪除時,它也不一定被調用。Python文檔中用不穩定來描述__del__()方法的這種行為,并且提供了額外的關于異常處理的注釋:運行期的異常會被忽略,相對地,會使用sys.stderr打印一個警告。
基于上面的這些原因,通常更傾向于使用上下文管理器,而不是實現__del__()。
2.7.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.7.2 循環引用和垃圾回收
下面是一種常見的循環引用的情形。一個父類包含一個子類的集合,同時集合中的每個子類實例又包含父類的引用。
下面我們用這兩個類來看看循環引用。
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)) )
一個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引用。
2.7.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種響應方法。因為試圖創建這個引用的對象時很可能馬上就會被刪除。
2.7.4 __del__()和close()方法
__del__()最常見的用途是確保文件被關閉。
通常,包含文件操作的類都會有類似下面這樣的代碼。
__del__ = close
這會保證__del__()方法同時也是close()方法。
其他更復雜的情況最好使用上下文管理器。詳情請看第5章“可調用對象和上下文的使用”,我們會在第5章提供更多和上下文管理器有關的信息。