1. 程式人生 > 其它 >流暢的python——8 物件引用、可變性和垃圾回收

流暢的python——8 物件引用、可變性和垃圾回收

八、物件引用、可變性和垃圾回收

每個變數都有標識、型別和值。物件一旦建立,它的標識絕不會變;可以把標識理解為物件在記憶體中的地址。is運算子比較兩個物件的標識;id() 函式返回物件標識的整數表示。

每個 Python 物件都有標識、型別和值。只有物件的值會不時變化。

作者:其實,物件的型別也可以變,方法只有一種:為 __class__ 屬性指定其他類。但這是在作惡,我後悔加上這個腳註了。

物件 ID 的真正意義在不同的實現中有所不同。在 CPython 中,id() 返回物件的記憶體地址,但是在其他 Python 直譯器中可能是別的值。關鍵是,ID 一定是唯一的數值標註,而且在物件的生命週期中絕不會變。

其實,程式設計中很少使用 id() 函式。標識最常使用 is 運算子檢查,而不是直接比較 ID。

== 與 is

== 比較兩個物件儲存的資料,is 比較的是物件的標識。

is None
is not None

is 運算子比 == 速度快,因為它不能過載,所以 Python 不用尋找並呼叫特殊方法,而是直接比較兩個整數 ID。而 a == b 是語法糖,等同於 a.__eq__(b)。繼承自 object 的 __eq__ 方法比較兩個物件的 ID,結果與 is 一樣。但是多數內建型別使用更有意義的方式覆蓋了 __eq__方法,會考慮物件屬性的值。相等性測試可能涉及大量處理工作,例如,比較大型集合或巢狀層級深的結構時。

元組的相對不可變性

元組與多數 python集合一樣,儲存的是物件的引用。

如果引用的元素是可變的,即便元組本身不可變,元素依然可變。也就是說,元組的不可變性其實是指tuple資料結構的物理內容(即儲存的引用)不可變,與引用的物件無關。元組中不可變得是元素的標識。

而 str、bytes、array.array 等單一型別序列是扁平的,它們儲存的不是引用,而是在連續的記憶體中儲存資料本身。

元組的相對不可變性,導致了有些元組不可雜湊。

預設做淺複製

In [17]: l1
Out[17]: [3, [55, 44], (7, 8, 9)]

In [18]: l2 = list(l1)  # 淺複製

In [19]: l2
Out[19]: [3, [55, 44], (7, 8, 9)]

In [20]: l1 == l2
Out[20]: True

In [21]: l1 is l2
Out[21]: False

In [22]: l3 = l1[:]

In [23]: l3
Out[23]: [3, [55, 44], (7, 8, 9)]

In [24]: l1 is l3
Out[24]: False
    
In [28]: l1[1] = 0  # 地址換了!!!

In [29]: l1
Out[29]: [3, 0, (7, 8, 9)]

In [30]: l2
Out[30]: [3, [55, 44], (7, 8, 9)]

In [31]: l3
Out[31]: [3, [55, 44], (7, 8, 9)]

In [33]: l1 = [3, [55, 44], (7, 8, 9)]

In [34]: l2 = list(l1)  # 淺複製

In [35]: l3 = l1[:]  # 淺複製

In [36]: l1[1].append(333)

In [37]: l2
Out[37]: [3, [55, 44, 333], (7, 8, 9)]

In [38]: l3
Out[38]: [3, [55, 44, 333], (7, 8, 9)]

可變物件:+= 是就地加

不可變物件:+= 是生成一個新的物件,將加的結果賦值給新物件

為任意物件做深複製和淺複製

深複製:副本不共享內部物件的引用

copy.deepcopy 深複製

copy.copy 淺複製

class Bus:
    def __init__(self, passengers=None):
        if passengers is None:
            self.passengers = []
        else:
            self.passengers = list(passengers)
    def pick(self, name):
        self.passengers.append(name)
    def drop(self, name):
        self.passengers.remove(name)
In [44]: bus1 = Bus(['a','b'])

In [45]: bus1.p_l
Out[45]: ['a', 'b']

In [46]: import copy

In [47]: bus2 = copy.copy(bus1)

In [48]: bus3 = copy.deepcopy(bus2)

In [49]: bus2.p_l
Out[49]: ['a', 'b']

In [50]: bus3.p_l
Out[50]: ['a', 'b']

In [52]: bus1.pick('ccc')

In [53]: bus1.p_l
Out[53]: ['a', 'b', 'ccc']

In [54]: bus2.p_l
Out[54]: ['a', 'b', 'ccc']

In [55]: bus3.p_l
Out[55]: ['a', 'b']

注意,一般來說,深複製不是件簡單的事。如果物件有迴圈引用,那麼這個樸素的演算法會進入無限迴圈。deepcopy 函式會記住已經複製的物件,因此能優雅地處理迴圈引用

此外,深複製有時可能太深了。例如,物件可能會引用不該複製的外部資源或單例值。我們可以實現特殊方法 __copy__() __deepcopy__(),控制 copy 和 deepcopy 的行為,詳情參見 copy 模組的文件(http://docs.python.org/3/library/copy.html)。

函式的引數作為引用時

Python 唯一支援的引數傳遞模式是共享傳參(call by sharing)。多數面嚮物件語言都採用這一模式,包括 Ruby、Smalltalk 和 Java(Java 的引用型別是這樣,基本型別按值傳參)。

共享傳參指函式的各個形式引數獲得實參中各個引用的副本。也就是說,函式內部的形參是實參的別名

這種方案的結果是,函式可能會修改作為引數傳入的可變物件,但是無法修改那些物件的標識(即不能把一個物件替換成另一個物件)。

不要使用可變型別作為引數的預設值

沒有指定初始值,多個物件會使用預設的同一個列表,相互影響。

出現這個問題的根源是,預設值在定義函式時計算(通常在載入模組時),因此預設值變成了函式物件的屬性。因此,如果預設值是可變物件,而且修改了它的值,那麼後續的函式呼叫都會受到影響。

防禦可變引數

如果定義的函式接受可變引數,應該謹慎考慮呼叫方是否期望修改傳入的值。

最少驚訝原則

除非這個方法確實想修改原來的引數物件,否則,就要想一下是否應該修改。如果不確定,建立副本。

del 和 垃圾回收

del 語句刪除物件的引用,而不是物件。

del 命令可能會導致物件被當作垃圾回收,但是僅當刪除的變數儲存的是物件的最後一個引用,或者無法得到物件時。重新繫結也可能會導致物件的引用數量歸零,導致物件被銷燬。

__del__ 特殊方法,但是它不會銷燬例項,不應該在程式碼中呼叫。即將銷燬例項時,Python 直譯器會呼叫 __del__ 方法,給例項最後的機會,釋放外部資源。自己編寫的程式碼很少需要實現 __del__ 程式碼,有些 Python 新手會花時間實現,但卻吃力不討好,因為 __del__ 很難用對。詳情參見 Python 語言參考手冊中“DataModel”一章中 del 特殊方法的文件(https://docs.python.org/3/reference/datamodel.html#object.del)。

在 CPython 中,垃圾回收使用的主要演算法是引用計數。

每個物件都會統計有多少引用指向自己。當引用計數歸零時,物件立即就被銷燬:CPython 會在物件上呼叫__del__ 方法(如果定義了),然後釋放分配給物件的記憶體。

A. Jesse Jiryu Davis 寫的“PyPy, Garbage Collection, and a Deadlock”一文(https://emptysqua.re/blog/pypy-garbage-collection-and-a-deadlock/)對 __del__ 方法的恰當用法和不當用法做了討論。

弱引用

正是因為有引用,物件才會在記憶體中存在。當物件的引用數量歸零後,垃圾回收程式會把物件銷燬。但是,有時需要引用物件,而不讓物件存在的時間超過所需時間。這經常用在快取中。

弱引用不會增加物件的引用數量。引用的目標物件稱為所指物件(referent)。因此我們說,弱引用不會妨礙所指物件被當作垃圾回收。弱引用在快取應用中很有用,因為我們不想僅因為被快取引用著而始終儲存快取物件。

示例展示瞭如何使用 weakref.ref 例項獲取所指物件。如果物件存在,呼叫弱引用可以獲取物件;否則返回 None。

控制檯 :變數 :_ 用於接收沒有接收的值。

WeakValueDictionary 簡介

weakref 模組的文件(http://docs.python.org/3/library/weakref.html)指出,weakref.ref 類其實是低層介面,供高階用途使用,多數程式最好使用 weakref 集合和 finalize。也就是說,應該使用 WeakKeyDictionary、WeakValueDictionary、WeakSet 和 finalize(在內部使用弱引用),不要自己動手建立並處理 weakref.ref 例項。我們在示例中那麼做是希望藉助實際使用 weakref.ref 來褪去它的神祕色彩。但是實際上,多數時候 Python 程式都使用 weakref 集合。

WeakValueDictionary 類 實現的是一種可變對映,裡面的值是物件的弱引用。被引用的物件在程式中的其他地方被當做垃圾回收後,對應的鍵會自動從 WeakValueDictionary 中刪除。因此,WeakValueDictionary 經常用於快取。

In [94]: zzz = 111

In [95]: for zzz in [1,2]:
    ...:     print(zzz)
    ...:
1
2

In [96]: zzz
Out[96]: 2

弱引用的侷限

不是每個 Python 物件都可以作為弱引用的目標(或稱所指物件)。基本的 list 和 dict 例項不能作為所指物件,但是它們的子類可以輕鬆地解決這個問題:

class MyList(list):
    """list的子類,例項可以作為弱引用的目標"""
    
a_list = MyList(range(10))
# a_list可以作為弱引用的目標
wref_to_a_list = weakref.ref(a_list)

set 例項可以作為所指物件,因此例項 8-17 才使用 set 例項。使用者定義的型別也沒問題,這就解釋了示例 8-19 中為什麼使用那個簡單的 Cheese 類。但是,int 和 tuple 例項不能作為弱引用的目標,甚至它們的子類也不行。

這些侷限基本上是 CPython 的實現細節,在其他 Python 直譯器中情況可能不一樣。這些侷限是內部優化導致的結果。

python 對不可變型別施加的把戲

元組 t 來說,t[:] 不建立副本,而是返回同一個物件的引用。此外,tuple(t) 獲得的也是同一個元組的引用

In [100]: a = [1,2,3]

In [102]: b = a[:]

In [103]: a is b  # 列表是淺拷貝
Out[103]: False

In [104]: c = (1,2,3)

In [105]: d = tuple(c)

In [106]: d
Out[106]: (1, 2, 3)

In [107]: c is d  # 元組就是同一個元組的引用
Out[107]: True

In [108]: d = c[:]

In [109]: d is c
Out[109]: True

In [110]: d = c[1:]

In [111]: d is c
Out[111]: False

str、bytes 和 frozenset 例項也有這種行為。注意,frozenset 例項不是序列,因此不能使用 fs[:](fs 是一個 frozenset 例項)。但是,fs.copy() 具有相同的效果:它會欺騙你,返回同一個物件的引用,而不是建立一個副本

In [112]: a = 'aaa'

In [113]: b = a[:]

In [114]: a is b
Out[114]: True

In [115]: c = copy.copy(a)

In [116]: c is a
Out[116]: True

In [117]: d = copy.deepcopy(a)

In [118]: d is a
Out[118]: True

copy 方法不會複製所有物件,這是一個善意的謊言,為的是介面的相容性:這使得 frozenset 的相容性比 set 強。

兩個不可變物件是同一個物件還是副本,反正對終端使用者來說沒有區別。

共享字串字面量是一種優化措施,稱為駐留(interning)。CPython 還會在小的整數上使用這個優化措施,防止重複建立“熱門”數字,如 0、-1 和 42。注意,CPython 不會駐留所有字串和整數,駐留的條件是實現細節,而且沒有文件說明。

千萬不要依賴字串或整數的駐留!比較字串或整數是否相等時,應該使用 ==,而不是 is。駐留是 Python 直譯器內部使用的一個特性。

本節討論的把戲,包括 frozenset.copy() 的行為,是“善意的謊言”,能節省記憶體,提升直譯器的速度。別擔心,它們不會為你帶來任何麻煩,因為只有不可變型別會受到影響。或許這些細枝末節的最佳用途是與其他 Python 程式設計師打賭,提高自己的勝算。

可以在自己的類中定義 __eq__ 方法,決定 == 如何比較例項。如果不覆蓋__eq__ 方法,那麼從 object 繼承的方法比較物件的 ID,因此這種後備機制認為使用者定義的類的各個例項是不同的。

處理不可變的物件時,變數儲存的是真正的物件還是共享物件的引用無關緊要。如果 a == b 成立,而且兩個物件都不會變,那麼它們就可能是相同的物件。這就是為什麼字串可以安全使用駐留。僅當物件可變時,物件標識才重要。