面試官:Python 對象的垃圾回收策略是什么?

作者 | wedo實驗君
來源 | Python中文社區(qū)
1. 引言
值傳遞和引用傳遞。python的對象引用也是學(xué)習(xí)python過程中需要特別關(guān)注的一個知識點(diǎn),特別是對函數(shù)參數(shù)傳遞,可能會引起不必要的BUG。本文將對引用做一個梳理,內(nèi)容涉及如下:變量和賦值 可變對象和不可變對象 函數(shù)參數(shù)的引用 淺拷貝和深拷貝 垃圾回收 弱引用
2. python引用
2.1 變量和賦值
is或者比較id()的判斷是否引用的是同一個內(nèi)存地址的變量。==是比較兩個對象的內(nèi)容是否相等,即兩個對象的值是否相等is同時檢查對象的值和內(nèi)存地址??梢酝ㄟ^is判斷是否是同一個對象id()列出變量的內(nèi)存地址的編號
#?這個例子a?和?b?兩個變量共同指向了同一個內(nèi)存空間
a?=?[1,?2,?3]
c?=?[1,?2,?3]
print(a?is?c)?#?False
print(a?==?c)?#?True
b?=?a
a.append(5)
print(a?is?b)?#?True
初始化賦值:變量的每一次初始化,都開辟了一個新的空間,將新內(nèi)容的地址賦值給變量
變量賦值:引用地址的傳遞
2.2 可變對象和不可變對象
可變對象包括字典dict、列表list、集合set、手動聲明的類對象等 不可變對象包括數(shù)字int float、字符str、None、元組tuple等
list 可變對象,內(nèi)容變更地址不變 a?=?[1,?2,?3]
print(id(a))
a.append(5)
print(id(a))不可變對象(常用的共享地址或緩存) #?較小整數(shù)頻繁被使用,python采用共享地址方式來管理
a?=?1
b?=?1
print(a?is?b)?#?True
#?對于單詞類str,python采用共享緩存的方式來共享地址
a?=?'hello'
b?=?'hello'
print(a?is?b)?#?True不可變對象(不共享地址) a?=?(1999,?1)
b?=?(1999,?1)
print(a?is?b)?#?False
a?=?'hello?everyone'
b?=?'hello?everyone'
print(a?is?b)?#?False元組的相對不可變型
#?元組的里元素是可變,改變可變的元素,不改變元組的引用
a?=?(1999,?[1,?2])
ida?=?id(a)
a[-1].append(3)
idb?=?id(a)
print(ida?==?idb)?#?True
a?=?[1,?2,?3]
print(id(a))
a?=?a?+?[5]
print(id(a))
#?前后兩個變量a,?已經(jīng)不是同一個地址了
2.3 函數(shù)參數(shù)的引用
def?func(d):
????d['a']?=?10
????d['b']?=?20????#?改變了外部實參的值????????
????d?=?{'a':?0,?'b':?1}??#?賦值操作,?局部d貼向了新的標(biāo)識
????print(d)?#?{'a':?0,?'b':?1}
d?=?{}
func(d)
print(d)?#?{'a':?10,?'b':?20}
class?bus():
????def?__init__(self,?param=[]):
????????self.param?=?param
????????
????def?test(self,?elem):
????????self.param.append(elem)
b?=?bus([2,?3])
b.param?#?[2,?3]
c?=?bus()
c.test(3)
c.param?#?[3]
d?=?bus()
d.param?#?[3]??#?c?中修改了默認(rèn)值的引用的內(nèi)容
2.4 淺拷貝和深拷貝
淺拷貝:只復(fù)制頂層的對象,對于有嵌套數(shù)據(jù)結(jié)構(gòu),內(nèi)部的元素還是原有對象的引用,這時候需要特別注意 深拷貝:復(fù)制了所有對象,遞歸式的復(fù)制所有對象。復(fù)制后的對象和原來的對象是完全不同的對象。對于不可變對象來說,淺拷貝和深拷貝都是一樣的地址。但是對于嵌套了可變對象元素的情況,就有所不同
test_a?=?(1,?2,?3)
test_b?=?copy.copy(test_a)
test_c?=?copy.deepcopy(test_a)
print(test_a?is?test_b)?#?True
print(test_a?is?test_c)?#?True
test_a[2].append(5)?#?改變不可變對象中可變元素的內(nèi)容
print(test_a?is?test_b)?#?True
print(test_a?is?test_c)?#?False
print(test_c)?#?(1,?2,?[3,?4])
l1?=?[3,?[66,?55,?44],?(2,?3,?4)]
l2?=?list(l1)?#?l2是l1的淺拷貝
#?頂層改變不會相互影響,因為是兩個不同對象
l1.append(50)?
print(l1)?#?3,?[66,?55,?44],?(2,?3,?4),?50]
print(l2)?#?[3,?[66,?55,?44],?(2,?3,?4)]
#?嵌套可變元素,淺拷貝共享一個地址
l1[1].append(100)
print(l1)?#?[3,?[66,?55,?44,?100],?(2,?3,?4),?50]
print(l2)?#?[3,?[66,?55,?44,?100],?(2,?3,?4)]
#?嵌套不可變元素,不可變元素的操作是創(chuàng)建一個新的對象,所以不影響
l1[2]?+=?(2,3)
print(l1)?#?[3,?[66,?55,?44,?100],?(2,?3,?4,?2,?3),?50]
print(l2)?#[3,?[66,?55,?44,?100],?(2,?3,?4)]
2.5 垃圾回收
引用計數(shù):python可以給所有的對象(內(nèi)存中的區(qū)域)維護(hù)一個引用計數(shù)的屬性,在一個引用被創(chuàng)建或復(fù)制的時候,讓python,把相關(guān)對象的引用計數(shù)+1;相反當(dāng)引用被銷毀的時候就把相關(guān)對象的引用計數(shù)-1。當(dāng)對象的引用計數(shù)減到0時,認(rèn)為整個python中不會再有變量引用這個對象,所以就可以把這個對象所占據(jù)的內(nèi)存空間釋放出來了??梢酝ㄟ^ sys.getrefcount()來查看對象的引用分代回收: 分代回收主要是為了提高垃圾回收的效率。對象的創(chuàng)建和消費(fèi)的頻率不一樣。由于python在垃圾回收前需要檢測是否是垃圾,是否回收,然后再回收。當(dāng)對象很多的時候,垃圾檢測的耗時變得很大,效率很低。python采用的對對象進(jìn)行分代,按不同的代進(jìn)行不同的頻率的檢測。代等級的規(guī)則根據(jù)對象的生命時間來判斷,比如一個對象連續(xù)幾次檢測都是可達(dá)的,這個對象代的等級高,降低檢測頻率。python中默認(rèn)把所有對象分成三代。第0代包含了最新的對象,第2代則是最早的一些對象 循環(huán)引用:一個對象直接或者間接引用自己本身,引用鏈形成一個環(huán)。這樣改對象的引用計數(shù)永遠(yuǎn)不可能為0。所有能夠引用其他對象的對象都被稱為容器(container). 循環(huán)引用只能發(fā)生容器之間發(fā)生. Python的垃圾回收機(jī)制利用了這個特點(diǎn)來尋找需要被釋放的對象。
import?sys
a?=?[1,?2]
b?=?a
print(sys.getrefcount(a))?#?3?命令本身也是一次引用
del?b
print(sys.getrefcount(a))?#?2?
3. 弱引用
應(yīng)用在緩存中,只存在一定的時間存在。當(dāng)它引用的對象存在時,則對象可用,當(dāng)對象不存在時,就返回None 不增加引用計數(shù),在循環(huán)引用使用,就降低內(nèi)存泄露的可能性
import?weakref
a_set?=?{0,1}
wref?=?weakref.ref(a_set)?#?建立弱引用
print(wref())?#?{0,1}
a_set?=?{2,?3,?4}?#?原來的a_set?引用計數(shù)為0,垃圾回收
print(wref())?#?None?#?所值對象被垃圾回收,?弱引用也消失為None
4. 總結(jié)
對象賦值就完成引用,變量是地址引用式的變量 要時刻注意,所以引用可變對象對象的改變,是否導(dǎo)致共同引用的變量值得變化 函數(shù)會修改是可變對象的實參 淺拷貝只是copy頂層,如果存在內(nèi)部嵌套可變對象,要注意,copy的還是引用 對象的引用計數(shù)為0時,就開始垃圾回收 弱引用為增加引用計數(shù),與被所指對象共存亡,而不影響循環(huán)引用
評論
圖片
表情
