今天來聊聊工作上發現的神奇東西,當我們要複製一個物件時,常會用到三種方式,賦值(Assignment)、淺層複製(Shallow Copy)與深層複製(Deep Copy),我們來聊聊他們之間的差異。
首先,參考自Python的官方文件,官方對於這三個名詞的定義如下:
- 賦值(Assignment): 用於將名稱(重)綁定到特定值。
- 淺層複製(Shallow Copy): 淺層複製建構一個新的複合物件,然後(在儘可能的範圍內)將原始物件中找到的物件的參照插入其中。
- 深層複製(Deep Copy): 深層複製建構一個新的複合物件,然後遞迴地將在原始物件裡找到的物件的副本插入其中。
再來我們一一的來探討這些複製方法的不同之處
賦值(Assignment)
簡單來說,就是使用賦值運算子(=)幫物件取名字,或是別名。Ex: A=1
我們假設有一個物件
List_1 = [[0,1],2,3]
這時候Python會
- 1.建立一個物件
- 2.變數List_1引用該物件
才會完成賦值
再來如果我們將
List_2 = List_1
這時候記憶體的運作如下圖所示
這時候可以看到List_2只是一個對象物件的參照,也就是別名的概念,他們指向一樣的記憶體位置。
list_1 = [[0,1],2,3]
list_2 = list_1
list_2[2] = 4
print('list_1=', list_1)
print('list_2=', list_2)
Output:
list_1= [[0, 1], 2, 4]
list_2= [[0, 1], 2, 4]
如上的範例所示,由於是一樣的記憶體位置,因此當我們更新了新list的值,原list也會一起改變。
我們可以使用Python的id方法去取得記憶體的位址
print('Memory Address of list_1: ', id(list_1))
print('Memory Address of list_2: ', id(list_2))
Output:
Memory Address of list_1: 1924319328448
Memory Address of list_2: 1924319328448
由此可知他們完全是指向同一個地方,因此若使用這個方法時,需要注意更新新的list的同時,原list也會一起被更新。
淺層複製(Shallow Copy)
淺層複製簡單來說,Python會先建構一個新的物件,再將原始物件中的子物件參照插入其中,我們先看個範例。
import copy
list_1 = [[0,1],2,3]
list_2 = copy.copy(list_1) #淺層複製
list_2[0][0] = 1 #原始物件中的物件(子物件)
list_2[2] = 4 #原始物件
print('list_1=', list_1)
print('list_2=', list_2)
Output:
list_1= [[1, 1], 2, 3]
list_2= [[1, 1], 2, 4]
我們可以使用copy這個套件來做到淺層複製,首先我們可以看到list_1的第一個index (list_1[0])有包含另外一個子物件([0,1]),這個就是文件中所提及的物件中的物件,記憶體的運作如下圖所示:
所以當我們執行copy方法後,會先複製出一個新的物件包含了2,3,再來再將原始物件中找到的物件(子物件[0,1])參照插入其中。
所以,我們可以看到當我們更新原始物件中的子物件[0,1]的值後,原始物件的子物件也會同時被更新;而當我們更新原始物件中的值之後,原始物件的值並不會被更新,是因為Python已經將這兩個物件分開保存在記憶體中了。
我們來看看他們的記憶體位置的差別
#list物件記憶體位置
print('Memory Address of list_1: ', id(list_1))
print('Memory Address of list_2: ', id(list_2))
Output:
Memory Address of list_1: 1924319685632
Memory Address of list_2: 1924318735744
很明顯的,兩個list物件所指到的記憶體位置是不同的,再來我們來看子物件的記憶體位置
#list子物件記憶體位置
print('Memory Address of list_1_sub-object: ', id(list_1[0]))
print('Memory Address of list_2_sub-object: ', id(list_2[0]))
Output:
Memory Address of list_1_sub-object: 1924318736000
Memory Address of list_2_sub-object: 1924318736000
就如果我們上面所說的,可以看到兩個子物件的記憶體位置是一樣的。
深層複製(Deep Copy)
深層複製簡單來說,就是會直接建構一個新的複合物件,然後將所有子物件也同時新增副本並插入其中,我們先來看個範例:
import copy
list_1 = [[0,1],2,3]
list_2 = copy.deepcopy(list_1) #深層複製
list_2[0][0] = 1 #原始物件中的物件(子物件)
list_2[2] = 4 #原始物件
print('list_1=', list_1)
print('list_2=', list_2)
Output:
list_1= [[0, 1], 2, 3]
list_2= [[1, 1], 2, 4]
我們使用copy套件中的deepcopy方法來做深層複製,可以看到不論是對新物件本身或是其子物件做更新,都不會影響到原來的物件,記憶體運作如下圖所示:
很清楚地可以看到,使用深層複製出來的新物件會與原物件完全不同,所以新物件更新後,完全不會影響到原物件的值,我們一樣來看一下他們的記憶體位置:
#list物件記憶體位置
print('Memory Address of list_1: ', id(list_1))
print('Memory Address of list_2: ', id(list_2))
Output:
Memory Address of list_1: 1924319685632
Memory Address of list_2: 1924318464192
可以看到兩個list是指向不同的記憶體位置,我們再深入查看子物件的記憶體位置
#list子物件記憶體位置
print('Memory Address of list_1_sub-object: ', id(list_1[0]))
print('Memory Address of list_2_sub-object: ', id(list_2[0]))
Output:
Memory Address of list_1_sub-object: 1924319739456
Memory Address of list_2_sub-object: 1924318464512
由此可確認,子物件也會做複製,存在不同的記憶體位置當中。
建議
今天介紹了三種不同的複製方法,如果真的要做複製,在記憶體足夠的情況下,建議可以先使用深層複製,將新物件獨立出原物件,以避免日後程式發生難以解決的bug。
參考資料
Python官方文件: https://docs.python.org/zh-tw/3/library/copy.html
Assignment, Shallow Copy, Or Deep Copy?: https://towardsdatascience.com/assignment-shallow-or-deep-a-story-about-pythons-memory-management-b8fad87bfa6c
copy in Python (Deep Copy and Shallow Copy): https://www.geeksforgeeks.org/copy-python-deep-copy-shallow-copy/