python3[进阶]8.对象引用、可变性和垃圾回收
文章目錄
- 8.1變量不是盒子
- 8.2 標(biāo)識,相等性和別名
- 8.2.1 在==和is之間選擇
- 8.2.2 元組的相對不可變性
- 8.3 默認(rèn)做淺復(fù)制
- (拓展)為任意對象做深復(fù)制和淺復(fù)制
- 深拷貝和淺拷貝有什么具體的區(qū)別呢?
- 8.4 函數(shù)的參數(shù)作為引用時
- 8.4.1 不要使用可變類型作為參數(shù)的默認(rèn)值
- 總結(jié)(閱讀)
- 8.4.2 防御可變參數(shù)
- 8.5 del和垃圾回收
- 8.6 弱引用
8.1變量不是盒子
python變量類似于Java中的引用型變量,因此最好把他們理解為附注在對象上的標(biāo)注.
a = [1,2,3] b = a a.append(7) print(b)輸出為:
[1, 2, 3, 7] // 可以發(fā)現(xiàn),a和b引用同一個列表,而不是那個列表的副本因為變量只不過是標(biāo)注,所以可以為對象貼上多個標(biāo)注,貼的多個標(biāo)注就是別名.
8.2 標(biāo)識,相等性和別名
每個變量都有標(biāo)識,類型和值.對象一旦創(chuàng)建,它的標(biāo)識一定不會變;可以把標(biāo)識(ID)理解為對象在內(nèi)存中的地址.
is運算符比較兩個對象的標(biāo)識;
id()函數(shù)返回對象標(biāo)識的整數(shù)表示.
標(biāo)識最常使用is運算符檢查,而不是直接比較ID.
charles = {'name':'charles', 'born':'1832'} lewis = charles print(lewis is charles) print(id(charles), id(lewis)) lewis['balence'] = 950 print(charles) alex = {'name': 'charles', 'born': '1832', 'balence': 950} print(alex == charles) print(alex is charles)輸出:
True 140640659352168 140640659352168 {'name': 'charles', 'born': '1832', 'balence': 950} True //比較兩個對象,結(jié)果相同,這是因為dic類的__eq__方法就是這樣實現(xiàn)的 False //但是他們是不同的對象,標(biāo)識不同. //可以發(fā)現(xiàn),charles和lewis綁定同一個對象,alex綁定另外一個對象8.2.1 在==和is之間選擇
==運算符比較兩個對象的值(對象中保存的數(shù)據(jù)),而is比較對象的標(biāo)識(標(biāo)識就是在內(nèi)存中的位置)
在變量和"單例值"之間比較時,應(yīng)該使用is.可以使用is檢查變量綁定的值是不是None.
is 運算符比 == 速度快,因為它不能重載,所以 Python 不用尋找并調(diào)用特殊方法,而是直接比較兩個整數(shù) ID。而 a == b 是語法糖,等同于 a.eq(b)。繼承自 object 的__eq__ 方法比較兩個對象的 ID,結(jié)果與 is 一樣。但是多數(shù)內(nèi)置類型使用更有意義的方式覆蓋了 eq 方法,會考慮對象屬性的值。相等性測試可能涉及大量處理工作。
8.2.2 元組的相對不可變性
元組與多數(shù) Python 集合(列表、字典、集,等等)一樣,保存的是對象的引用。而 str、bytes 和 array.array 等單一類型序列是扁平的,它們保存的不是引用,而是在連續(xù)的內(nèi)存中保存數(shù)據(jù)本身(字符、字節(jié)和數(shù)字)。
元組的不可變性其實是指tuple數(shù)據(jù)結(jié)構(gòu)的物理內(nèi)容(保存的引用)不可變,與引用的對象無關(guān).
復(fù)制對象時,相等性和一致性之間的區(qū)別有更深入的影響。副本與源對象相等,但是ID不同。可是,如果對象中包含其他對象,那么應(yīng)該復(fù)制內(nèi)部對象嗎?可以共享內(nèi)部對象嗎?這些問題沒有唯一的答案。
8.3 默認(rèn)做淺復(fù)制
l1 = [3, [55, 44], (7, 8, 9)] l2 = list(l1) print(l2) print(l2 == l1) print(l2 is l1) print(l2[2] is l1[2]) l3 =l1[:] print(l3) print(l3 == l1) print(l3 is l1) print(l3[2] is l1[2]) l4 = l1 print(l4 == l1) print(l4 is l1) l1[1].append(66) print(l1) print(l2) print(l3) print(l4)輸出結(jié)果如下:
上圖發(fā)現(xiàn):
- l2和l3對應(yīng)著關(guān)于l1的淺拷貝,l4直接將l1起了一個別名,也就是說l4和l1指向了同一個對象。
- 淺拷貝對于內(nèi)層引用有影響,即內(nèi)層引用還是指向了同一個對象。
- [3, [55, 44], (7, 8, 9)] //list(l1)創(chuàng)建l1的副本
True //副本和源列表相等
對于列表和其他可變序列來說,還可以使用更簡潔的l3 = l1[:]語句來創(chuàng)建副本
然而,構(gòu)造方法或者[:] 做的是淺復(fù)制(就是復(fù)制了最外層容器,副本中的元素是源容器中元素的引用).如果所有的元素都是不可變的,那么這樣沒有問題,如果有可變的元素,會出現(xiàn)問題.
輸出:
l1: [3, [66, 44], (7, 8, 9), 100] l2: [3, [66, 44], (7, 8, 9)] l1: [3, [66, 44, 33, 22], (7, 8, 9), 100] l2: [3, [66, 44, 33, 22], (7, 8, 9, 10, 11)] # 對**元組**來說,+=運算符創(chuàng)建一個新元組,然后重新綁定給變量l2[2].現(xiàn)在l1和l2中最后位置上的元組不是同一個對象. # 總結(jié):對+=和×=所做的增量賦值來說,如果左邊的變量綁定的是不可變對象,會創(chuàng)建新對象;如果是可變對象,會就地修改。如圖:
(拓展)為任意對象做深復(fù)制和淺復(fù)制
淺復(fù)制沒什么問題,但有時我們需要的是深復(fù)制(即副本不共享內(nèi)部對象的引用)。
import copy class Bus:def __init__(self, passengers=None):if passengers is None:self.passengers = []else:self.passengers = passengersdef pick(self,name):self.passengers.append(name)def drop(self,name):self.passengers.remove(name) bus1 = Bus(['Alice', 'Bill', 'Claire', 'David']) bus2 = copy.copy(bus1) bus3 = copy.deepcopy(bus1) print(id(bus1), id(bus2), id(bus3)) bus1.drop('Bill') print(bus2.passengers) print(id(bus1.passengers), id(bus2.passengers),id(bus3.passengers)) print(bus3.passengers)輸出:
140694379943920 140694379943976 140694379944088 ['Alice', 'Claire', 'David'] 140694377362824 140694377362824 140694377336776 ['Alice', 'Bill', 'Claire', 'David']結(jié)果如圖:
使用 copy 和 deepcopy,創(chuàng)建 3 個不同的 Bus 實例。
審查 passengers 屬性后發(fā)現(xiàn):
- bus1 和 bus2 共享同一個列表對象,因為 bus2 是bus1 的淺復(fù)制副本。
- bus3 是 bus1 的深復(fù)制副本,因此它的 passengers 屬性指代另一個列表。
從上面可以發(fā)現(xiàn),深拷貝和淺拷貝都會創(chuàng)建不同的對象,深拷貝是完全拷貝一個新的對象,淺拷貝不會拷貝子對象。
深拷貝和淺拷貝有什么具體的區(qū)別呢?
import copy a = [1,2,3,['a','b','c']] b = copy.copy(a) c = copy.deepcopy(a) a[3].append('d') print(a) print(b) print(c)結(jié)果如圖:
從上圖我們可以發(fā)現(xiàn),
- copy.deepcopy()會完全拷貝一個新的對象出現(xiàn);
- copy.copy()不會拷貝其子對象,也就是說,如果原來的對象里面又包含別的對象的引用,則這個新的對象還是會指向這個舊的內(nèi)層引用。
總結(jié):
copy.copy() 淺復(fù)制,不會拷貝其子對象,修改子對象,將受影響 .
copy.deepcopy() 深復(fù)制,將拷貝其子對象,修改子對象,將不受影響.
8.4 函數(shù)的參數(shù)作為引用時
python唯一支持的參數(shù)傳遞模式是共享傳參(call by sharing).
共享傳參指函數(shù)的各個形式參數(shù)獲得實參中各個引用的副本,也就是說,函數(shù)內(nèi)部的形參是實參的別名。
這種方案的結(jié)果是,函數(shù)可能會修改作為參數(shù)傳入的可變對象,但是無法修改那些對象的標(biāo)識(即不能把一個對象替換成另一個對象)。
輸出:
3 1 2 [1, 2, 3, 4] [1, 2, 3, 4] [3, 4] (10, 20, 30, 40) (10, 20) (30, 40)我們發(fā)現(xiàn)數(shù)字x沒變,列表a變了,元組t沒變
8.4.1 不要使用可變類型作為參數(shù)的默認(rèn)值
可選參數(shù)可以有默認(rèn)值,這是python函數(shù)定義的一個很好的特性.但是我們應(yīng)該避免使用可變的對象作為參數(shù)的默認(rèn)值.
class HauntedBus:"""備受幽靈乘客折磨的校車"""def __init__(self, passengers=[]):self.passengers = passengersdef pick(self,name):self.passengers.append(name)def drop(self,name):self.passengers.remove(name)bus1 = HauntedBus(['Alice','Bill']) print(bus1.passengers) bus1.pick('Charlie') bus1.drop('Alice') print(bus1.passengers) bus2 = HauntedBus() bus2.pick('Carrie') print(bus2.passengers) bus3 = HauntedBus() print(bus3.passengers) bus3.pick('Dive') print(bus2.passengers) print(bus2.passengers is bus3.passengers) print(bus1.passengers)輸出:
['Alice', 'Bill'] ['Bill', 'Charlie'] ['Carrie'] ['Carrie']//bus3一開始是空的,但是默認(rèn)列表卻不為空 ['Carrie', 'Dive'] True ['Bill', 'Charlie']問題在于,沒有指定初始乘客的HauntedBus實例會共享同一個乘客列表。
使用可變類型作為函數(shù)參數(shù)的默認(rèn)值有危險,因為如果就地修改了參數(shù),默認(rèn)值也就變了,這樣會影響以后使用默認(rèn)值的調(diào)用。
修正的方法很簡單:在__init__方法中,傳入passengers參數(shù)時,應(yīng)該把參數(shù)值的副本賦值給self.passengers,
總結(jié)(閱讀)
- 第一類:t2 = t1[:]或者 t2 = list(t1) 都是了新的對象t2.但是內(nèi)層引用還是指向同一個對象。
- 第二類:l2 = copy.copy(l1)
8.4.2 防御可變參數(shù)
如果定義的函數(shù)接收可變參數(shù),應(yīng)該謹(jǐn)慎考慮調(diào)用方是否期望修改傳入的參數(shù)。
示例 8-15 一個簡單的類,說明接受可變參數(shù)的風(fēng)險
測試一下
>>> basketball_team = ['Sue', 'Tina', 'Maya', 'Diana', 'Pat'] >>> bus = TwilightBus(basketball_team) >>> bus.drop('Tina') >>> bus.drop('Pat') >>> basketball_team ['Sue', 'Maya', 'Diana']發(fā)現(xiàn):下車的學(xué)生從籃球隊中消失了!
TwilightBus 違反了設(shè)計接口的最佳實踐,即“最少驚訝原則”。學(xué)生從校車中下車后,她的名字就從籃球隊的名單中消失了,這確實讓人驚訝。
這里的問題是,校車為傳給構(gòu)造方法的列表創(chuàng)建了別名。正確的做法是,校車自己維護(hù)乘客列表。修正的方法很簡單:在 init 中,傳入 passengers 參數(shù)時,應(yīng)該把參數(shù)值的副本賦值給 self.passengers,像示例 8-8 中那樣做(8.3 節(jié))。
def __init__(self, passengers=None): if passengers is None:self.passengers = [] else:self.passengers = list(passengers) ?? 創(chuàng)建 passengers 列表的副本;如果不是列表,就把它轉(zhuǎn)換成列表。在內(nèi)部像這樣處理乘客列表,就不會影響初始化校車時傳入的參數(shù)了。此外,這種處理方式還更靈活:現(xiàn)在,傳給 passengers 參數(shù)的值可以是元組或任何其他可迭代對象,例如set 對象,甚至數(shù)據(jù)庫查詢結(jié)果,因為 list 構(gòu)造方法接受任何可迭代對象。
8.5 del和垃圾回收
del 語句刪除名稱,而不是對象。del 命令可能會導(dǎo)致對象被當(dāng)作垃圾回收,但是僅當(dāng)刪除的變量保存的是對象的最后一個引用,或者無法得到對象時。 重新綁定也可能會導(dǎo)致對象的引用數(shù)量歸零,導(dǎo)致對象被銷毀。
在 CPython 中,垃圾回收使用的主要算法是引用計數(shù)。實際上,每個對象都會統(tǒng)計有多少引用指向自己。當(dāng)引用計數(shù)歸零時,對象立即就被銷毀:CPython 會在對象上調(diào)用__del__ 方法(如果定義了),然后釋放分配給對象的內(nèi)存。
CPython 2.0 增加了分代垃圾回收算法,用于檢測引用循環(huán)中涉及的對象組——如果一組對象之間全是相互引用,即
使再出色的引用方式也會導(dǎo)致組中的對象不可獲取。Python 的其他實現(xiàn)有更復(fù)雜的垃圾回收程序,而且不依賴引用計數(shù),這意味著,對象的引用數(shù)量為零時可能不會立即調(diào)用__del__ 方法。
8.6 弱引用
正是因為有引用,對象才會在內(nèi)存中存在。當(dāng)對象的引用數(shù)量歸零后,垃圾回收程序會把對象銷毀。但是,有時需要引用對象,而不讓對象存在的時間超過所需時間。這經(jīng)常用在緩存中。
弱引用不會增加對象的引用數(shù)量。引用的目標(biāo)對象稱為所指對象(referent)。因此我們說,弱引用不會妨礙所指對象被當(dāng)作垃圾回收。
弱引用在緩存應(yīng)用中很有用,因為我們不想僅因為被緩存引用著而始終保存緩存對象。
總結(jié)
以上是生活随笔為你收集整理的python3[进阶]8.对象引用、可变性和垃圾回收的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 第一百三十五期:如何模拟一次阿里双11秒
- 下一篇: python (第八章)补充-可迭代对象