程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式師世界 >> 編程語言 >  >> 更多編程語言 >> Python

[Python] 對象引用、可變性和垃圾回收

編輯:Python

《流暢的Python》盧西亞諾·拉馬略 第8章 讀書筆記

8.1 變量不是盒子

為了理解Python中的賦值語句,應該始終先讀右邊。對象在右邊創建或獲取,在此之後左邊的變量才會綁定到對象上,這就像為對象貼上標注。
因為變量只不過是標注,所以無法阻止為對象貼上多個標注。

8.2 標識、相等性和別名

每個變量都有標識、類型和值。對象一旦創建,它的標識絕不會變;你可以把標識理解為對象在內存中的地址。
==運算符比較兩個對象的值(對象中保存的數據)
is比較對象的標識,id()函數返回對象標識的整數表示

最常使用is檢查變量綁定的值是否為None
x is None​​
x is not None​​

is運算符比==速度快,因為它不能重載,所以Python不用尋找並調用特殊方法,而是直接比較兩個整數ID。而a==b是語法糖,等同於a.__eq__(b)。繼承自object的__eq__方法比較兩個對象的ID,結果與is一樣。但是多數內置類型使用更有意義的方式覆蓋了__eq__方法,會考慮對象屬性的值。

8.3 默認做淺復制

構造方法或[:]做的是淺復制(即復制了最外層容器,副本中的元素是源容器中元素的引用)。如果所有元素都是不可變的,那麼這樣沒有問題,還能節省內存。但是,如果有可變的元素,可能就會導致意想不到的問題。

為任意對象做深復制和淺復制
有時我們需要的是深復制(即副本不共享內部對象的引用)。copy模塊提供的deepcopy和copy函數能為任意對象做深復制和淺復制。為了演示copy() 和deepcopy() 的用法,下例定義了一個簡單的類,Bus。這個類表示運載乘客的校車,在途中乘客會上車或下車。
【例】校車乘客在途中上車和下車
​​

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)

交互式控制台中 創建一個Bus實例和兩個副本

>>> import copy
>>> from Bus import Bus
>>> bus1 = Bus(['Alice', 'Bill', 'Claire', 'David'])
>>> bus2 = copy.copy(bus1)
>>> bus3 = copy.deepcopy(bus1)
>>> id(bus1), id(bus2), id(bus3)
(140137100109976, 140137100110144, 140137100110312)
>>> bus1.drop('Bill')
>>> bus1.passengers
['Alice', 'Claire', 'David']
>>> bus2.passengers
['Alice', 'Claire', 'David']
>>> bus3.passengers
['Alice', 'Bill', 'Claire', 'David']
>>> id(bus1.passengers), id(bus2.passengers), id(bus3.passengers)
(140137100086024, 140137100086024, 140137136242952)
>>>

說明:
line4, bus2是bus1的淺復制
line5, bus3是bus1的深復制
line8, bus1中的'Bill'下車
line12, bus2中也沒有'Bill'
line15, 審查passengers屬性後發現,bus1和bus2共享同一個列表對象

8.4 函數的參數作為引用時

8.4.1 不要使用可變類型作為參數

以例1為基礎定義一個新類HauntedBus,然後修改__init__方法。passengers的默認值不是None,而是[],這樣就不用像之前那樣使用if判斷了。這個“聰明的舉動”會讓我們陷入麻煩。
【例2】一個簡單的類,說明可變默認值的危險

class HauntedBus:
"""備受幽靈乘客折磨的校車"""
def __init__(self, passengers=[]):
self.passengers = passengers
def pick(self, name):
self.passengers.append(name)
def drop(self, name):
self.passengers.remove(name)

line3  如果沒傳入passengers參數,使用默認綁定的列表對象,一開始是空列表。
line4  這個賦值語句把self.passengers變成passengers的別名,而沒有傳入passengers參數時,self.passengers是默認列表的別名。
line6、8  在self.passengers上調用.append()和.remove()方法時,修改的其實是默認列表,它是函數對象的一個屬性。
HauntedBus的詭異行為 --> 

>>> from Bus import HauntedBus
>>> bus1 = HauntedBus(['Alice', 'Bill'])
>>> bus1.passengers
['Alice', 'Bill']
>>> bus1.pick('Charlie')
>>> bus1.drop('Alice')
>>> bus1.passengers
['Bill', 'Charlie']
>>>
>>> bus2 = HauntedBus()
>>> bus2.pick('Carrie')
>>> bus2.passengers
['Carrie']
>>>
>>> bus3 = HauntedBus()
>>> bus3.passengers
['Carrie']
>>> bus3.pick('Dave')
>>> bus3.passengers
['Carrie', 'Dave']
>>>
>>> bus2.passengers
['Carrie', 'Dave']
>>> bus2.passengers is bus3.passengers
True
>>> bus1.passengers
['Bill', 'Charlie']
>>>

說明:

line7 目前沒什麼問題,bus1沒有出現異常。
line10 一開始,bus2是空的,因此把默認的空列表賦值給self.passengers。
line15 bus3一開始也是空的,因此還是賦值默認的列表。
line16 但是默認列表不為空!
line22 登上bus3的Dave出現在bus2中。
line24 問題是,bus2.passengers和bus3.passengers指代同一個列表。
line26 但bus1.passengers是不同的列表。

問題在於,沒有指定初始乘客的HauntedBus實例會共享同一個乘客列表。
實例化HauntedBus時,如果傳入乘客,會按預期運作。
但是不為HauntedBus指定初始乘客的話,self.passengers變成了passengers參數默認值的別名。

審查HauntedBus.__init__對象,看看它的__defaults__屬性中的那些幽靈學生:

>>> dir(HauntedBus.__init__)
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
>>>
>>> HauntedBus.__init__.__defaults__
(['Carrie', 'Dave'],)
>>>

我們可以驗證bus2.passengers是一個別名,它綁定到HauntedBus.__init__.__defaults__屬性的第一個元素上:

>>> HauntedBus.__init__.__defaults__[0] is bus2.passengers
True
>>>


可變默認值導致的這個問題說明了為什麼通常使用None作為接收可變值的參數的默認值
在例1中,__init__方法檢查passengers參數的值是否為None,如果是就把一個新的空列表賦值給self.passengers。如果passengers不是None,正確的實現會把passengers的副本賦值給self.passengers。

8.4.2 防御可變參數

在__init__中,傳入passengers參數時,應該把參數值的副本賦值給self.passengers

除非這個方法確實想修改通過參數傳入的對象,否則在類中直接把參數賦值給實例變量之前一定要三思,因為這樣會為參數對象創建別名。

8.5 del和垃圾回收

del語句刪除名稱(對象的引用),而不是對象。
對象才會在內存中存在是因為有引用。當對象的引用數量歸零後,垃圾回收程序會把對象銷毀。

僅當刪除的變量保存的是對象的最後一個引用,或者無法得到對象時,del命令會導致對象被當作垃圾回收。
重新綁定也可能會導致對象的引用數量歸零,導致對象被銷毀。

8.6 弱引用

有時需要引用對象,而不讓對象存在的時間超過所需時間,這經常用在緩存中。
弱引用不會增加對象的引用數量  ==>  弱引用不會妨礙所指對象被當作垃圾回收

引用的目標對象稱為所指對象(referent),如果所指對象不存在了,返回None

>>> import weakref
>>> a_set = {0, 1}
>>> wref = weakref.ref(a_set) #創建弱引用對象wref
>>> wref
<weakref at 0x7f86f289af98; to 'set' at 0x7f86f0b799e8>
>>> wref() #調用wref()返回的是被引用的對象,{0, 1}。因為這是控制台會話,所以{0, 1}會綁定給_變量
{0, 1}
>>> _
{0, 1}
>>>
>>> a_set = {2, 3, 4} #a_set不再指代{0, 1}集合,因此集合的引用數量減少了。但是_變量仍然指代它。
>>>
>>> _
{0, 1}
>>> wref() #調用wref()依舊返回{0, 1}。
{0, 1}
>>>
>>> wref() is None #計算這個表達式時,{0, 1}存在,因此wref()不是None。
False
>>> _ #隨後_綁定到結果值False。現在{0, 1}沒有強引用了。
False
>>>
>>> wref() is None #因為{0, 1}對象不存在了,所以wref()返回None。
True
>>>

說明:多數程序最好使用WeakKeyDictionary、WeakValueDictionary、WeakSet和finalize(在內部使用弱引用),不要自己動手創建並處理weakref.ref實例。

8.6.1 WeakValueDictionary簡介

WeakValueDictionary類實現的是一種可變映射,裡面的值是對象的弱引用。被引用的對象在程序中的其它地方被當作垃圾回收後,對應的鍵會自動從WeakValueDictionary中刪除。因此,WeakValueDictionary經常用於緩存。

class Cheese:
    def __init__(self, kind):
        self.kind = kind
    def __repr__(self):
        return 'Cheese(%r)'%self.kind

把catalog中的各種奶酪載入WeakValueDictionary實現的stock中。刪除catalog後,stock中只剩下一種奶酪了。你知道為什麼帕爾馬干酪(Parmesan)比其他奶酪保存的時間長嗎?代碼後面的提示中有答案。
交互式環境測試:

>>> from Cheese import Cheese
>>> import weakref
>>> stock = weakref.WeakValueDictionary()   #stock是WeakValueDictionary實例
>>> catalog = [Cheese('Red Leicester'), Cheese('Tilsit'),Cheese('Brie'), Cheese('Parmesan')]   
>>> for cheese in catalog:
...     stock[cheese.kind] = cheese   #stock把奶酪的名稱映射到catalog中Cheese實例的弱引用上
>>> sorted(stock.keys())
['Brie', 'Parmesan', 'Red Leicester', 'Tilsit']   #stock是完整的。
>>> del catalog
>>> sorted(stock.keys())
['Parmesan']   #刪除catalog之後,stock中的大多數奶酪都不見了,這是WeakValueDictionary的預期行為。為什麼不是全部呢?
>>>
>>> del cheese
>>> sorted(stock.keys())
[]
>>>

臨時變量引用了對象,這可能會導致該變量的存在時間比預期長。通常,這對局部變量來說不是問題,因為它們在函數返回時會被銷毀。但上述舉例中,for循環中的變量cheese是全局變量,除非顯式刪除,否則不會消失。

與WeakValueDictionary對應的是WeakKeyDictionary,後者的鍵是弱引用。

8.6.2 弱引用的局限

不是每個Python對象都可以作為弱引用的目標(或稱所指對象)。
基本的list和dict實例不能作為所指對象,但是它們的子類可以輕松地解決這個問題;
set實例可以作為所指對象,用戶定義的類型也沒問題;
int和tuple實例不能作為弱引用的目標,甚至它們的子類也不行。


  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式師世界 All Rights Reserved