變量賦值相當於什麼?
Python中的變量賦值就相當於貼標籤。例如創建一個列表a = [1, 2, 3],那麼列表[1, 2, 3]的一個標籤爲a。此時將變量a賦值給變量b,也就相當於列表[1, 2, 3]有了兩個標籤a和b,內容和地址沒有改變,只是換了個叫法。例子如下:
# 例1
a = [1, 2, 3]
b = a # 傳遞列表對象的引用 相當於[1, 2, 3]貼了兩個標籤 分別是a和b
b.append(4) # 對b操作 發現a也改變了
print(a) # [1, 2, 3, 4]
不能把b = a理解爲開闢了兩個不一樣的空間,應該理解爲a和b是同一個東西,只不過叫法不同。
內容相等和對象相同不是一回事
Python中判斷兩個變量的標識(可以簡單理解爲指向的地址,或者說引用的對象)是否相等,用的是'is',而判斷兩個對象的內容是否相同是用'=='號。下面的例2分別創造了一樣內容的字典a和b,它們內容是相同的但是屬於不同的對象。
# 例2
a = {'name': 'James', 'age': 18}
b = {'name': 'James', 'age': 18}
print(a == b) # True 內容相同 所以是對的
print(a is b) # False 因爲各自創建了一個字典 所以不是相同的對象
使用'=='號的時候是在調用內置方法__eq__,用來判斷兩個對象的內容是否相同。這個方法是可以重寫的,比如你可以重寫後用'=='判斷你定義的兩個向量是否相等。'is'用來判斷兩個對象的標識是否相同,一個對象一旦創建,標識就不會改變,標識可以簡單理解爲創建對象的地址。使用id()函數就可以返回對象標識的整數值。所以上面的代碼分別創建了兩個字典(即使內容相同),標識不一樣,自然a is b就是False。那如果不是各自創建,而是賦值呢?
# 例3
a = {'name': 'James', 'age': 18}
b = a # 對象引用賦值給b 那麼a、b引用的內容相同、對象也相同
print(a == b) # True
print(a is b) # True
print(id(a), id(b)) # 2510206295640 2510206295640 不同的解釋器可能計算的id的方式不同
對於例2,各自創建了一個字典,屬於不同對象,id不同,因此互不干擾。
對於例3,只創建了一個字典,賦值使得a、b所引用的對象相同,因此改一個另一個也會隨着改變。即例4:
# 例4
a = {'name': 'James', 'age': 18}
b = a # 對象引用賦值給b 那麼a、b引用的內容相同、對象也相同
b['country'] = 'SZ' # 對b進行改動 發現a也變了
print(a) # {'name': 'James', 'age': 18, 'country': 'SZ'}
print(b) # {'name': 'James', 'age': 18, 'country': 'SZ'}
print(id(a), id(b)) # 1654996164184 1654996164184 對象是相同的
說句題外話,'=='號能重載,而'is'不能重載,因此通常情況下判斷None用if x is None會比if x == None快。
隨意賦值並改變內容的後果
既然賦值就是貼標籤,那麼很可能一個對象就有很多個標籤,只改其中一個標籤,是不是就會全部都改變呢?其實要分情況討論…
Python的數據類型中有可變的和不可變的之分。可變的元素有列表、字典等,不可變的有數字、元組等。可變的元素意味着改變了其中的內容,它的對象還是自己。例如列表是可變的,你加進去一個數字,該列表的對象標識(id)還是原來的。不可變的元素就不一樣了,元組是不可變的,一旦定義了就不能變,如果想要加一個數字進去,那麼得到的必定不是原來的對象了。
複雜點來說,就是,可變的,隨你怎麼變還是這個對象;不可變的,一旦變了就不是同一個對象了。
舉例5說明可變的對象,如果隨意賦值,任意操作其中一個,牽一髮而動全身,後果很可怕!
# 例5
a = [1, 2, 3] # 列表是可變對象
b = a # 傳遞列表對象的引用 相當於[1, 2, 3]貼了兩個標籤 分別是a和b
b.append(4) # 對b操作 發現a也改變了
print(a) # [1, 2, 3, 4]
print(b) # [1, 2, 3, 4]
print(id(a), id(b)) # 2286888405896 2286888405896 說明對象相同
這就警告我們,如果要複製某個列表去使用(例如增刪改),回頭還要使用原來的列表時,絕對不能直接用賦值(如b = a)。解決方法見下一模塊。
例6說明,如果是不可變的對象,直接賦值,任意操作其中一個變量,不會影響其他的變量,後果還OK!
# 例6
a = 1 # 數字是不可變的元素
b = a # 數字1被貼上a、b的標籤 a、b屬於同一對象
print(a, b) # 1 1 同一對象 因此值相同
print(id(a), id(b)) # 1738370064 1738370064 id相同 還是同一對象
b = 2 # 對不可變的元素更改內容 會導致重新開闢空間 產生新的對象
print(a, b) # 1 2
print(id(a), id(b)) # 1738370064 1738370096 可見不可變的元素更改後與原對象不同
不可變對象賦值後改變就相當於拷貝另一份了,之後就互不相干了,因此看上去還算比較安全,不會改一個就全都改了,但是後果還OK!不是完全OK,因爲改一次你就會重新開闢新的空間,創建新的對象,因此蠻費空間的。
安全賦值怎麼操作?
上面講到可變元素賦值的時候,多個變量改其中一個其他也會變,那麼怎麼避免呢?如果賦值後還需要用到原來的對象,那麼建議進行拷貝操作,也就是複製一份。
複製一份的操作也要分情況討論。如果可變元素的內容全部是不可變的時候,那就直接拷貝,例如一個列表(可變)內都是整數(不可變)。
# 例7
a = [1, 2, 3] # 列表是可變的 內部元素是整數 不可變
b = list(a) # 拷貝生成新的列表對象
c = a[:] # 拷貝生成新的列表對象
print(a, b, c) # [1, 2, 3] [1, 2, 3] [1, 2, 3]
print(id(a), id(b), id(c)) # 2099645172616 2099646935176 2099646889736 對象不同 互不相干
如果可變元素的內容包含可變的元素,例如列表裏包含列表,如[1, 2, [3, 4]],或是不可變元素裏包含可變的,例如元組裏面含有列表,如(1, 2, [3, 4]),那麼上述的拷貝方法也存在安全隱患。換句話說,遇到嵌套在裏面的可變元素,普通拷貝還是有隱患。
淺拷貝和深拷貝
Python的字典、元組、列表等都是容器序列,他們不直接存放值,而是存放裏面對象的引用。比如a = [1, 2, [3, 4]],列表a中的第三個元素是[3, 4],列表a內存中存放的並不是3, 4的值,而是列表[3, 4]對象的引用。但是直接拷貝這個列表時,如使用b = list(a),或者b = a[:],得到的是淺拷貝。淺拷貝字面理解就是拷貝的不充分,只是複製了元素的對象的引用,用上面的例子就是指只有拷貝了1,2和列表[3, 4]的對象的引用(整型數據也有對象的引用)。前面我們知道,對於1,2是不可變元素,所以你一旦改變了就會創建新的空間存儲,標識就會改變,因此不是原來的對象,但是對於列表[3, 4],就算你改變了列表[3, 4]裏面元素的內容,他的對象的引用還是不會變,因此存在風險。
# 例8
import copy
a = [1, 2, [3, 4]] # 可變的列表嵌套着另一個可變的列表
b = copy.copy(a) # 類似於b = a[:]的淺拷貝
print(a, b) # [1, 2, [3, 4]] [1, 2, [3, 4]] 淺拷貝一份 內容一模一樣
print(id(a), id(b)) # 2797099397192 2797099399240 外層容器複製了一份 因此對象不同
print(id(a[2]), id(b[2])) # 2797099351880 2797099351880 a、b內部存儲的是列表[3, 4]對象的引用 因此淺拷貝後對象的引用沒變
b[2].append(5) # 對b中的列表[3, 4]加一個數字5
print(a, b) # [1, 2, [3, 4, 5]] [1, 2, [3, 4, 5]] 對b[2]操作 結果a[2]卻變了 說明淺拷貝後改內部的可變元素也會改變原來的拷貝前的對象的元素
print(id(a), id(b)) # 2797099397192 2797099399240 外部列表還是原來各自不同的id
print(id(a[2]), id(b[2])) # 2797099351880 2797099351880 內部的列表還是原來相同的id
同理,哪怕是不可變的元組如a = (1, 2, [3, 4]),裏面嵌套一個可變的列表即[3, 4],淺拷貝後,改其中一個元組內的可變元素,比如往[3, 4]里加一個數字,也會使得拷貝的元組同樣發生改變。這就是元組的相對不可變性,元組的定義是說不可變的,是指保存的對象的引用不可變,與引用的內容無關,所以元組內部的元素是可能改變的。用上面的例子就是元組裏面的列表[3, 4]對象的引用不可變,但是列表[3, 4]裏面的內容不管多一個數少一個數,不考慮。
淺拷貝有隱患,就相當於拷貝的不充分,只拷貝到了內部元素的對象的引用,但是對象引用的內容更改還是沒能獨立完成。那麼就有了深拷貝的出現,深拷貝不僅僅是拷貝到內部元素的對象的引用,而是接着去拷貝到每一個元素內容,就相當於完全複製一份,不管是否內部包含可變元素了。深拷貝安全,但是比較費空間。
# 例9
import copy
a = (1, 2, [3, 4]) # 不可變的元組嵌套着另一個可變的列表
b = copy.deepcopy(a) # 深拷貝 a、b完全不相關了
print(a, b) # (1, 2, [3, 4]) (1, 2, [3, 4]) 深拷貝一份 內容一模一樣
print(id(a), id(b)) # 2531887773736 2531887842216 外層容器複製了一份 因此對象不同
print(id(a[2]), id(b[2])) # 2531887873864 2531887921224 內部存儲的是列表[3, 4]對象的引用 深拷貝後不同了 相互獨立
b[2].append(5) # 對b內的列表[3, 4]加一個數字5
print(a, b) # (1, 2, [3, 4]) (1, 2, [3, 4, 5]) 對b[2]操作 a[2]不變 說明深拷貝後各自獨立
print(id(a), id(b)) # 2531887773736 2531887842216 外部元組還是原來各自不同的id
print(id(a[2]), id(b[2])) # 2531887873864 2531887921224 內部的元組還是原來各自不同的id