python對象引用和垃圾回收


變量="標簽"

變量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章


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM