上面文章有介紹 Python 動態類型 共享引用等相關知識,有這個基礎,我們來深入研究一下 Python 賦值,拷貝的原理,有涉及到可變類型和不可變類型的不同處理。更多 Pyton 進階系列文章,請參考 Python 進階學習 玩轉數據系列
內容提要:
不可變類型:
Python 中不可變類型有整形,字符串,元組等,一次性創建,不能被修改,只要有修改,就是在重新創建新的對象。
例如:Python底層是c語言寫的,c語言沒有字符串的說法,字符串是字符數組,所以在內存址是字符數組的方式
a = 'abc'
b = a
a = 'bcd'
語句 #1 a = ‘abc’
創建一個字符串數組 ‘abc’ 對象,將字符串數組的地址賦值給變量 a,數組的元素分別存儲字符 a,b,c.
語句 #2 b = a
將變量 a 指向的字符串數組的地址賦值給 b
語句 #3 a = ‘bcd’
創建一個新的字符串數組 ‘bcd’ 對象,將新的字符串數組的地址賦值給變量 a,這樣 a 的指向發生變化,但變量 b 依然還是指向字符串數組 ‘abc’ 對象。
圖解不可變類型內存存儲:
可變類型
Python 中可變類型有 list 列表,set 集合,dict 字典等,一次性創建,可以被修改。
例如:list 列表
a = [1,'hi',3]
a.append(100)
a[1] = 22
語句 #1 a = [1,‘hi’,3]
創建一個列表對象 和 3 個其它對象,列表中的元素分別存儲元素對象的地址。
將列表對象的地址賦值給變量 a
列表的第一個元素指向值為 1 的對象地址
列表的第二個元素指向 ‘hi’ 字符串數組對象地址,字符串數組中的元素分別存儲是字符 h, i
列表的第三個元素指向值為 3 的對象地址
語句 #2 a.append(100)
因為列表是可變對象,所以不用重現創建新的列表對象,在原有列表的基礎上追加一個元素,指向新對象 100 的地址。
語句 #3 a[1] = 22
因為列表是可變對象,所以不用重現創建新的列表對象,在原有列表的更改元素指向,將列表第二個元素指向另外一個對象 22 的地址。
圖解可變類型內存存儲:由上可見,不可變類型只有一層引用指向,可變類型至少有二層引用指向
。
不可變類型的拷貝 copy:
import copy
a = 'hello'
a_assign = a
a_copy = copy.copy(a)
a_copy_deep = copy.deepcopy(a)
print("id(a):{}".format(id(a)))
print("id(a_assign):{}".format(id(a_assign)))
print("id(a_copy):{}".format(id(a_copy)))
print("id(a_copy_deep):{}".format(id(a_copy_deep)))
輸出:
id(a):2557654519408
id(a_assign):2557654519408
id(a_copy):2557654519408
id(a_copy_deep):2557654519408
對於數字和字符串等不可變類型而言,賦值、淺拷貝和深拷貝無意義,因為其永遠指向同一個內存地址
。
可變類型的 copy:
import copy
a = [1,2,[3,4],5]
a_assign = a
a_copy = copy.copy(a) # same as a_assign = a[:]
a_copy_deep = copy.deepcopy(a)
a.append(66)
a[1] = 22
a[2][1] = 44
print("id(a):{}\t\ta:{}".format(id(a),a))
print("id(a_assign):{}\ta_assign:{}".format(id(a_assign),a_assign))
print("id(a_copy):{}\ta_copy:{}".format(id(a_copy),a_copy))
print("id(a_copy_deep):{}\ta_copy_deep:{}".format(id(a_copy_deep),a_copy_deep))
輸出:
id(a):2527859256200 a:[1, 22, [3, 44], 5, 66]
id(a_assign):2527859256200 a_assign:[1, 22, [3, 44], 5, 66]
id(a_copy):2527859283528 a_copy:[1, 2, [3, 44], 5]
id(a_copy_deep):2527859255752 a_copy_deep:[1, 2, [3, 4], 5]
語句 #1 a = [1,2,[3,4],5]
創建一個列表為父對象,將列表對象的地址賦值給變量 a
再分別創建值為 1,2,3,4,5 的對象,
還要創建一個list 列表子對象,其第一個元素指向值為 3 的對象,第二個元素指向值為 4 的對象。
該主鏈表第一個元素指向對象 1 的地址
該主鏈表第二個元素指向對象 2 的地址
該主鏈表第三個元素指向對象 列表 [3,4] 的地址
該主鏈表第四個元素指向對象 5 的地址
語句 #2 a_assign = a
將 變量 a 指向的主列表對象的地址賦值給變量 a_assign
語句 #3 a_copy = copy.copy(a)
淺拷貝,只拷貝變量 a 指向的對象的頂級對象,或者說:父級對象,不拷貝子對象。在內存中只額外創建第一層數據。
也就是創建一個新的主列表對象,其地址賦值給變量 a_copy ,該新列表裡元素是copy a 指向的列表對象的元素。
該新列表第一個元素指向對象 1 的地址
該新列表第二個元素指向對象 2 的地址
該新列表第三個元素指向對象 列表 [3,4] 的地址
該新列表第四個元素指向對象 5 的地址注意第二級子對象 **列表 [3,4]** 是沒有copy的
語句 #4 a_copy_deep = copy.deepcopy(a)
深拷貝,拷貝所有對象,頂級對象及其嵌套對象。或者說:父級對象及其子對象。在內存中將所有的數據重新創建一份(排除最後一層,即:python內部對字符串和數字的優化,即緩存,不用頻繁創建短數字,短字符串對象)
圖解:
語句 #5 a.append(66)
a 變量指向的列表對象追加一個新的元素指向值為 66 的對象,所以變量 a = [1,2,[3,4],5, 66
]
因為 變量 a 和 變量 a_assign 指向的是同一列表對象,所以 a_assign = [1,2,[3,4],5, 66
]
a_copy_deep 和 a_copy_deep 指向的是新創建的主列表對象,所以不受影響,是沒有變化的.
a_copy_deep = [1,2,[3,4],5]
a_copy_deep = [1,2,[3,4],5]
語句 #6 a[1] = 22
將變量 a 指向的列表中第二個元素指向另外一個新的對象 22,所以變量 a = [1,22
,[3,4],5], 66
]
因為 變量 a 和 變量 a_assign 指向的是同一列表對象,所以 a_assign = [1,22
,[3,4],5], 66
]
a_copy_deep 和 a_copy_deep 指向的是新創建的主列表對象,所以不受影響,是沒有變化的.
a_copy_deep = [1,2,[3,4],5]
a_copy_deep = [1,2,[3,4],5]
語句 #7 a[2][1] = 44
將變量 a 指向的列表第三個元素是一個子對象,子對象的第二個元素指向另外一個新的對象 44,所以變量 a = [1,22
,[3,44
],5], 66
]
因為 變量 a 和 變量 a_assign 指向的是同一列表對象,所以 a_assign = [1,22
,[3,44
],5], 66
]
因為變量 a_copy 只是拷貝了變量 a 指向的主對象列表,而沒有 copy(重新創建) 子對象,所以 a_copy 指向的列表對象中第三個元素還是對應 a 變量指向主對象中的第三個元素的子對象,所以子對象也應該發現變化, a_copy = [1,2,[3,44
],5]
a_copy_deep 是有重新創建子對象的,所以不受 a 指向的子對象變化影響,所以沒有變化 a_copy_deep = [1,2,[3,4],5]
圖解:
直接賦值:其實就是對象的引用(別名)。
淺拷貝(copy):拷貝父對象,不會拷貝對象的內部的子對象。
深拷貝(deepcopy): copy 模塊的 deepcopy 方法,完全拷貝了父對象及其子對象。
深淺拷貝都是對源對象的復制,占用不同的內存空間,即創建新的內存空間。
如果源對象只有一級目錄的話,源做任何改動,不影響深淺拷貝對象,如針對不可變類型,int,str。
如果源對象不止一級目錄的話,源做任何改動,都要影響淺拷貝,但不影響深拷貝,序列對象的切片其實是淺拷貝,即只拷貝頂級的對象,如對可變換類型, list,set,dict。