變量="標簽"
變量a和變量b引用同一個列表:
>>> a = [1, 2, 3] >>> b = a >>> a.append(4) >>> b [1, 2, 3, 4]
使用"標簽"很形象的解釋了變量 =========> 列表[1, 2, 3]是一個物品,而a和b都是給這個物品貼上的標簽。因此,改變a的內容,b的內容也改變了。
"is"和"=="
有一個人叫做李華,1997年生,身體情況工作信息記錄為info,有個小名叫"小華"。
>>> lihua = {'name':'lihua','born':'1997','information':'info'}
>>> xiaohua = lihua
>>> xiaohua is lihua
True
>>> id(xiaohua),id(lihua)
(2072419437304, 2072419437304)
>>> xiaohua['information'] = 'new_info'
>>> lihua
{'name': 'lihua', 'born': '1997', 'information': 'new_info'}
可見xiaohua和lihua指代同一個對象,假如有個冒充者(李華)說他是李華,身份信息一模一樣,記為anony。
>>> anony = {'name': 'lihua', 'born': '1997', 'information': 'new_info'}
>>> anony == lihua
True
>>> anony is lihua
False
此時使用"is"和"=="判斷結果是不同的。lihua和xiaohua綁定同一個對象,xiaohua是lihua的別名;而lihua和anony綁定不同對象。
"=="比較的是對象的值,而"is"比較對象的標識。
在Python中,對象的標識就是id()函數返回值,而is比較的就是這個返回值的整數表示。在Cpython中,id()返回的是對象的內存地址,在其他Python解釋器中可能是別的值。最主要的是,id()函數返回值在對象的生命周期中一定不會改變。
寫程序是一般關注值,因此==出現頻率較高,而在變量和單例值之間比較時應該使用is。除此之外,is運算符比==快,因為它不能重載,解釋器不需要尋找並調用特殊方法,直接比較整數id;a==b是語法糖,等同於a.__eq__(b),繼承自object的__eq__方法比較兩個對象的id,結果與is一樣,而覆蓋__eq__方法后結果可能就與is結果不同了。
元組是"可變的"
元組保存對象的引用,如果引用的元素是可變的,即使元組本身不可變,元素依然可變。也就是說,元組的不可變性其實是指tuple數據結構的物理內容(即引用)不可變,與引用的對象無關。
t1 = (1, 2, [30, 40]) t2 = (1, 2, [30, 40]) print(t1 == t2) True print(id(t1[-1])) 3031106993224 #標識 t1[-1].append(50) print(t1) (1, 2, [30, 40, 50]) #修改t1[-1]列表 print(id(t1[-1])) 3031106993224 #標識沒變 print(t1 == t2) False #值改變了,不相等
淺復制與深復制
默認作淺復制
復制列表最簡單的方式是使用內置類型的構造方法,以list為例:
l1 = [3, [40, 50], (6, 7, 8)] l2 = list(l1) print(l2) [3, [40, 50], (6, 7, 8)] print(l2 == l1) True #副本與原副本相等 print(l2 is l1) False #副本與原副本指代不同的對象
當然,可變序列都可以用 [:] 來復制,而無論是構造方法還是 [:] 復制都是淺復制(即復制了最外層容器,副本中的元素是源容器中元素的引用),如果元素是可變的,就會出現問題。
l1 = [3, [40, 50], (6, 7, 8)] l2 = list(l1) l1.append(99) l1[1].remove(50) print('l1:', l1) print('l2:', l2) l2[1] += [22, 33] l2[2] += (9, 10) print('l1:', l1) print('l2:', l2) #結果 l1: [3, [40], (6, 7, 8), 99] l2: [3, [40], (6, 7, 8)] #對比l1,由於淺復制,追加99對l2無影響,而對元組l1里面的可變對象[40, 50]執行刪除操作卻影響到了l1,說明l2和l1綁定同一個列表 l1: [3, [40, 22, 33], (6, 7, 8), 99] l2: [3, [40, 22, 33], (6, 7, 8, 9, 10)] #+=操作就地修改列表,因此l2與l1同時被修改,而+=對於元組這種不可變對象來說,會重新創建一個元組,重新綁定給l2,修改后,l2中的那個元組與l1中的不是同一個 print(id(l1[2])) print(id(l2[2])) print(id(l1[2])) print(id(l2[2])) #替換為打印id,發現l2的元組id最后改變了 2377750817600 2377750817600 2377750817600 2377749721512
淺復制容易,但有時會出現不想要也很意外的結果,就需要深復制。
為任意對象作淺復制和深復制
copy模塊提供copy用於淺復制和deepcopy用於深復制(副本不共享內部對象的引用)。
定義一個類bus表示校車,有乘客上車下車方法:
import 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) bus1 = Bus(['Alice', 'Bob', 'David']) #原校車 bus2 = copy.copy(bus1) #copy方法復制的校車 bus3 = copy.deepcopy(bus1) #deepcopy方法復制的校車 print(id(bus1), id(bus2), id(bus3)) bus1.drop('David') #bus1的David下車 print(bus2.passengers) print(id(bus1.passengers), id(bus2.passengers), id(bus3.passengers)) print(bus3.passengers) #結果 2765338083960 2765338084072 2765338083456 #三個不同的Bus對象 ['Alice', 'Bob'] #bus2的David消失了 2765338349448 2765338349448 2765337978376 #bus1,bus2共享同一個列表對象,bus3則有另一個列表 ['Alice', 'Bob', 'David'] #bus3沒有改變
一般來說,深復制不是一件簡單的事情。如果對象有循環引用,那么這個朴素算法會進入無限循環。deepcopy函數會記住已經復制的對象,因此能優雅的處理循環引用:
from copy import deepcopy a = [10, 20] b = [a , 30] a.append(b) print(a) c = deepcopy(a) print(c) [10, 20, [[...], 30]] [10, 20, [[...], 30]]
深復制有時處理得太深,對象可能會引用不該復制的外部資源或單例值,此時可以實現特殊方法__copy__()和__deepcopy__(),控制copy和deepcopy的行為。
函數的參數作為引用
python唯一支持的方式是共享傳參。類似於java的引用傳參。它是指函數各個形式參數獲得實參中各個引用的副本,即函數內部形參是實參的別名。
這樣,函數可能會修改作為參數傳入的可變對象,但是無法修改那些對象標識(即不能把一個對象替換為另一個對象)
不要使用可變類型作為參數默認值
可選參數可以有默認值,但應該避免使用可變對象作為參數默認值。如果使用可變參數,后果見例子:
定義一輛校車,passenger默認值不用None而用[ ]
class Bus: def __init__(self, passengers=[]): self.passengers = passengers def pick(self, name): self.passengers.append(name) def drop(self, name): self.passengers.remove(name) bus1 = Bus(['Alice', 'Bob']) #1車原兩人 print(bus1.passengers) bus1.pick('Jane') bus1.drop('Alice') print(bus1.passengers) #上一人下一人 bus2 = Bus() #2車空車 bus2.pick('David') print(bus2.passengers) #上一人 bus3 = Bus() #3車空車 bus3.pick('Mike') print(bus2.passengers) #上一人 print(bus2.passengers is bus3.passengers) print(bus1.passengers)
奇怪的現象出現了:
['Alice', 'Bob'] ['Bob', 'Jane'] ['David'] ['David', 'Mike'] True ['Bob', 'Jane']
1車正常行駛,3車出現”幽靈學生“,上二車的David出現在了3車。事實上,可看到bus2和bus3引用的是同一個乘客列表。
實例化Bus時,如果傳入乘客可以正常運作,但是不為Bus指定乘客的話,奇怪的事情發生,這是因為self.passengers變成了passengers參數默認值的別名。默認值在定義函數時計算,因而默認值變為了函數對象的屬性,如果默認值是可變對象,那么后續函數調用都會受到影響。審查Bus.__init__對象
print(Bus.__init__.__defaults__) #'David', 'Mike'成為默認乘客 (['David', 'Mike'],)
所以,如果定義的函數接受可變參數,應該慎重考慮調用方是否期望修改傳入的參數。例如校車寫成深復制那一節的樣子。
del和垃圾回收
del語句刪除名稱,或者說刪除標簽。(刪除一個物品的標簽,而不是刪除這個物品)del命令可能導致對象被當作垃圾回收,即滿足下列條件之一時:
1.刪除的變量保存的是對象的最后一個引用
2.無法得到對象
重新綁定也可能會導致對象的引用數量歸零,導致對象銷毀。
python采用引用計數算法來進行垃圾回收,每個對象都會統計有多少個引用指向自己,當引用計數器歸零時,對象就立即銷毀。python2.0采用分代垃圾回收算法,用於處理循環引用。
見下例:
import weakref s1 = {1, 2, 3} s2 = s1 def bye(): print('bye') ender = weakref.finalize(s1, bye) #注冊一個回調函數,在{1,2,3}銷毀時使用 print(ender.alive) del s1 print(ender.alive) s2 = 'helloworld' print(ender.alive) #結果發現del s1后,對象仍然存活,而s2重新綁定了對象,於是無法獲取對象,導致對象被銷毀 True True bye False
弱引用
有引用時對象才會在內存中存在。當對象的引用數量歸零后,垃圾回收程序會把對象銷毀。
弱引用不會增加對象引用數量,引用的目標對象稱為所指對象,因此,弱引用不會妨礙所指對象被當作垃圾回收(任何無引用的時候)。弱引用在緩存中很有用,因為我們不想因為被緩存引用着而始終保存緩存對象。
python提供weakref模塊來控制弱引用。
weakref.ref
import weakref import sys set1 = {1, 2} print(sys.getrefcount(set1)) #打印引用計數 wref = weakref.ref(set1) #創建弱引用 print(wref) #打印弱引用 print(sys.getrefcount(wref)) set2 = wref() #!!!弱引用時可調用對象,返回的是被引用的對象,若所指對象不存在則返回None print(set2 is set1) print(sys.getrefcount(set1)) set1 = None set2 = None print(wref)
結果:
2 <weakref at 0x0000024BADFA0408; to 'set' at 0x0000024BADEE99E8> 2 True 3 #調用弱引用返回被引用對象綁定到set2,所以引用顯示為3 <weakref at 0x0000024BADFA0408; dead> #弱引用失效
初始引用為2的原因是:當使用某個引用作為參數,傳遞給getrefcount()時,參數實際上創建了一個臨時的引用。
weakref.WeakValueDictionary
WeakValueDictionary類實現一種可變映射,里面的值是對象的弱引用,被引用對象在程序其他地方被當作垃圾回收后,對應的鍵會自動從WeakValueDictionary中刪除。
import weakref class Cheese: def __init__(self, kind): self.kind = kind def __repr__(self): return 'Cheese(%r)' % self.kind stock = weakref.WeakValueDictionary() catalog = [Cheese('A'), Cheese('B'), Cheese('C'), Cheese('D'), Cheese('E'), Cheese('A')] for cheese in catalog: stock[cheese.kind] = cheese print(sorted(stock.keys())) del catalog print(sorted(stock.keys())) del cheese print(sorted(stock.keys()))
結果:
['A', 'B', 'C', 'D', 'E'] ['A'] []
刪除引用后['A']奶酪還在,是因為臨時變量引用了對象,這可能導致該變量存在的時間比預期長。通常,這對局部變量來說不是問題,因為它們在函數返回時會被銷毀。示例中是全局變量,需顯式刪除才會消失。
Weak模塊還有proxy,WeakSet,WeakKeyDictionary等
//proxy(obj[,callback])函數來創建代理對象。使用代理對象就如同使用對象本身一樣,而不需要像ref那樣顯示調用
//WeakKeyDictionary的鍵是弱引用,它的實例可以為應用中其他部分擁有的對象附加元數據,這樣就無需為對象添加屬性
//WeakSet類保存元素弱引用的集合類,元素沒有強引用時,集合會把它刪除
以上來自《流暢的python》第8章
