標籤:port 項目 不可變對象 記錄 對象引用 決定 span 變數 引用計數
最近項目中遇到一個Python淺拷貝機制引起的bug,由於對於Python中對象引用、賦值、淺拷貝/深拷貝機制沒有足夠的認識,導致調試了很久才發現問題,這裡簡單記錄一下相關概念。
在Python的設計哲學中,Python中的每一個東西都是對象,都有一個ob_refcnt變數,這個變數維護著對對象的引用計數,決定著對象的建立與消亡。
所以在Python程式中,一般的賦值操作其實只是將左值指向了右值的引用,並不會建立新的對象,可以通過id函數查看Python中對象在記憶體中的唯一標識,以list對象為例,如下代碼:
>>> alist=[[1,2],3,4]>>> blist=alist>>> id(alist);id(blist) #alist/blist實際引用記憶體中的同一個list對象140357688098184140357688098184>>> blist.append(5)>>> blist[[1, 2], 3, 4, 5]>>> alist[[1, 2], 3, 4, 5] #由於實際引用同一個list對象,blist增加一個元素後,alist的取值實際上是完全一樣的>>> id(alist);id(blist)140357688098184140357688098184
在上面的代碼中,將alist的值賦給blist,其實只是把blist指向了alist在記憶體中的對象,兩者引用了同一個list對象,此時如果對blist append一個新元素,由於是指向同一個對象,alist的輸出結果一樣會變化。
通過slice文法或者copy模組的copy函數,可以實現淺拷貝--
>>> import copy>>> alist=[[1,2],3,4]>>> blist=alist[:]>>> clist=copy.copy(alist)>>> id(alist);id(blist);id(clist) #alist/blist/clist實際已經指向記憶體中的不同list對象140357691858696140357691897864140357720939912>>> id(alist[0]);id(blist[0]);id(clist[0]) #alist[0]/blist[0]/clist[0]三個子物件依然指向記憶體中的同一個list對象140357691897800140357691897800140357691897800>>> blist.append(5)>>> blist[[1, 2], 3, 4, 5]>>> alist[[1, 2], 3, 4] #blist對象值的變更,不會再影響到alist和clist>>> clist[[1, 2], 3, 4]>>> alist[0].append(‘a‘)>>> alist[[1, 2, ‘a‘], 3, 4]>>> blist[[1, 2, ‘a‘], 3, 4, 5] #由於實際引用同一對象,alist[0]子物件值的變更,也會從blist[0]/clist[0]上體現出來>>> clist[[1, 2, ‘a‘], 3, 4]>>> id(alist[1]);id(blist[1]);id(clist[1])109194881091948810919488
可以看到blist和clist本身已經是新的list對象,不再引用alist這個list對象,但是三個list中的子物件還是相同的引用,因為python中的淺拷貝只能拷貝父物件,不會拷貝對象內部的子物件。
通過copy模組中的copy.deepcopy函數可以實現深拷貝:
>>> alist=[[1,2],3,4]>>> blist=copy.deepcopy(alist)>>> id(alist);id(blist) #alist/blist已經引用記憶體中不同的list對象140357692023560140357691897608>>> blist.append(5)>>> blist[[1, 2], 3, 4, 5]>>> alist[[1, 2], 3, 4] #blist取值的變更,不會影響到alist>>> id(alist[0]);id(blist[0]) #alist{0]/blist[0]兩個子物件也已經引用記憶體中不同的list對象140357691897864140357691896136>>> alist[0].append(‘a‘)>>> alist[[1, 2, ‘a‘], 3, 4]>>> blist[[1, 2], 3, 4, 5] # alist[0]子物件值的變更,也不會再印象到blist[0]的值>>> id(alist[1]);id(blist[1])1091948810919488
可以看到,通過copy.deepcopy進行拷貝後,alist和blist指向不同的list對象,同時其子物件alist[0]/blist[0]也指向了不同的list對象,但是alist[1]/blist[1]還是指向相同的對象,這是因為3、4在Python中其實是不可變對象,相當於是常量,在Python中不可變對象只會存在唯一一份,所以無論淺拷貝/深拷貝,都是對同一個不可變對象進行的引用。
對於dict/set這些Python類型對象的賦值操作,也會存在類似的淺拷貝/深拷貝的問題,下面再以dict為例貼一下代碼:
引用賦值:
>>> adct={‘d‘:{1:2}, 3:4}>>> bdct=adct>>> id(adct);id(bdct) #adct/bdct實際引用記憶體中的同一個dict對象140357688090760140357688090760>>> id(adct[‘d‘]);id(bdct[‘d‘]) #adct[‘d‘]/bdct[‘d‘]兩個子物件實際引用記憶體中的同一個dict對象140357691897928140357691897928>>> bdct[‘d‘].update({‘a‘:‘b‘})>>> bdct{‘d‘: {1: 2, ‘a‘: ‘b‘}, 3: 4}>>> adct{‘d‘: {1: 2, ‘a‘: ‘b‘}, 3: 4} #由於實際指向同一個子物件,bdct[‘d‘]取值的變更會直接影響到adct的值
copy.copy淺拷貝:
>>> adct={‘d‘:{1:2}, 3:4}>>> bdct=copy.copy(adct)>>> id(adct);id(bdct) #adct/bdct引用不同的dict對象140357688082888140357720937544>>> id(adct[‘d‘]);id(bdct[‘d‘]) #adct[‘d‘]/bdct[‘d‘]兩個子物件依然指向記憶體中同一個dict對象140357688101704140357688101704>>> bdct[‘d‘].update({‘a‘:‘b‘})>>> bdct{‘d‘: {1: 2, ‘a‘: ‘b‘}, 3: 4}>>> adct{‘d‘: {1: 2, ‘a‘: ‘b‘}, 3: 4} #由於實際引用同一個子物件,bdct[‘d‘]子物件值的變更會直接影響到adct的值
copy.deepcopy深拷貝:
>>> adct={‘d‘:{1:2}, 3:4}>>> bdct=copy.deepcopy(adct)>>> id(adct);id(bdct) #adct/bdct本身已經引用不同的dict對象140357691897928140357688094152>>> id(adct[‘d‘]);id(bdct[‘d‘]) #adct/bdct的子物件引用了不同的dict子物件140357688090760140357688085896>>> bdct[‘d‘].update({‘a‘:‘b‘})>>> bdct{‘d‘: {1: 2, ‘a‘: ‘b‘}, 3: 4}>>> adct{‘d‘: {1: 2}, 3: 4} #bdct[‘d‘]子物件的變更不會再影響到adct[‘d‘]的值
Python中的對象引用、淺拷貝與深拷貝