古靈精怪的python——地址,淺拷貝與身份運算子

技術小能手發表於2018-08-13

首先丟擲一個問題,吸引讀者的閱讀興趣(如果您覺得這個不是問題,那麼這篇文章不適合您:)

請看如下程式碼:

>>> a = 3

>>> b = 3
>>> a == b

True
>>> a is b
True
>>> b = a

# 這沒問題
>>> a = 3
>>> a == b
# 這看起來也很合理

True
>>> a is b
True
>>> a is b

>>> a = (2,3)
>>> b = (2,3)
>>> b = a

False # ???why?
>>> a == b
True
>>> a is b
True

True # ???why?
>>> a == b

好了,整篇文章都是圍繞這個問題展開的。長久以來我都習慣用is而不用==來進行兩個物件的比較(python中一切皆物件) 直到今天出了一個bug後才瞭解到這兩者之間的不同,挖到python的一個大坑之餘,不禁出了一身冷汗。。。

is還是==

補充知識

id() 用於獲取物件在記憶體中的地址,並以十進位制展示出來。如:

>>> a = 3

>>> id(a)
140602638349720

>>> hex(id(a)) # 還原成我們看著更順眼的16進位制,但是本文以10進位制地址為主(因為懶)

`0x7fe09a503598`

顧名思義,is是“相同”,而==是指兩者之間的”相等“關係。所謂相同,比較的是兩者之間的在記憶體中的位置,

>>> a = 3

>>> id(a)
140602638349720

>>> b = 3 # b指向的是和a指向的同一塊地址(但是並不意味這改變了a,b也會相應改變)

>>> id(b)
140602638349720
140602638349720

>>> c = a # a的引用複製給c,在記憶體中其實是指向了用一個物件
>>> id(c)
>>> a is b
True
True

>>> a is c
True
>>> b is c

我們看到,上面a,b,c的地址相同,所以他們互相之間”相同“

而相等則兩者之間的數值對應相等

>>> a = 3

>>> b = a
>>> b

>>> a = 4
3
>>> b = [3]

>>> a = [3]
>>> id(a)
4351374112

4351374184
>>> id(b)
>>> a is b
>>> a[0] = 4

False 
>>> a == b
True
>>> b
[3]
>>> b = a # b就是a的引用,佔得是同一塊地址,而且當a的內容改變時,b也會隨之改變,這和上面

>>> a = [3]
# int物件不同,我也不知道為啥要這麼搞。
[4] 

>>> a[0] = 4
>>> b

很多同學看到這肯定是一鍋漿糊了,其實就是一個原則,能用==就不用is。除了一種情況,那就是判斷物件是否是None。

>>> if a is None:

... pass

淺拷貝和深拷貝

>>> a = [3]

>>> b = a[:] #通過切片賦值,返回的是a的淺拷貝

>>> id(a)
4351273944
>>> id(b)
140602638349720

4351374184
>>> id(a[0])
>>> a is b 

>>> id(b[0]) #list的地址不同
140602638349720
False
>>> a[0] is b[0] #淺拷貝,只拷貝了a的殼[],裡邊的內容仍然是同一個東西,同樣的id

True
>>> a == b
True
>>> a[0] == b[0]
True
>>> a[0] = 4 # 但是,b的內容不會隨著a的變化而變化
>>> b
[3]

淺拷貝拷貝了最外層容器,副本中的元素是原容器中元素的引用

我們再看一個例子

>>> Anndy = [`Anndy`, [`age`, 24]]

>>> Tom = Anndy[:]
>>> id(Anndy)

>>> Cindy = list(Anndy)
4351374040
4351374616

>>> id(Tom)
4351373968
>>> id(Cindy)
([`Anndy`, [`age`, 24]],[`Anndy`, [`age`, 24]],[`Anndy`, [`age`, 24]])

>>> print(Anndy, Tom, Cindy)
# 看起來是建立了三個不同的物件,因為他們的id各不相同
>>> print (Anndy, Tom, Cindy)

>>> Tom[0] = `Tom`
>>> Cindy[0] = `Cindy`
# 如果想修改某一個人的名字也沒有什麼問題

([`Anndy`, [`age`, 24]], [`Tom`, [`age`, 24]], [`Cindy`, [`age`, 24]])
# 現在我們想把Tom的年齡修改為12歲
>>> Tom[1][1] = 12
# 震驚!所有人的年齡都變成了12!!!

>>> print (Anndy, Tom, Cindy)
([`Anndy`, [`age`, 12]], [`Tom`, [`age`, 12]], [`Cindy`, [`age`, 12]])
>>> print ([id(x) for x in Anndy])
[4351366224, 4351374112] # 第一個姓名元素的地址不同,但是第二個列表是同一個

[4351366368, 4351374112] # 看第二個列表的地址
>>> print ([id(x) for x in Tom])
[4351323592, 4351374112] # 看第二個!
>>> print ([id(x) for x in Cindy])

構造方法或切片 [:] 做的是淺拷貝。如果所有元素都是不可變的(比如名字字串,修改的時候會重新建立物件,僅僅包括原子物件的元組也屬於這種情況),那麼這樣沒有問題,還能節省記憶體。但是,如果有可變的元素,可能就會導致意想不到的問題,正如剛剛,修改一個人的年齡,所有人的年齡都發生了變化。

所以,如果你想要深拷貝,應該這麼寫

>>> import copy

>>> Anndy = [`Anndy`, [`age`, 24]]

>>> Tom = copy.deepcopy(Anndy)
>>> print(Tom, Anndy)

>>> Tom[1][1] = 12
([`Anndy`, [`age`, 12]], [`Anndy`, [`age`, 24]]) #這樣寫就沒問題了

另外

不知道剛才你有沒有注意到

>>> a = 3

>>> b = 3
140602638349720

>>> id(a)
>>> id(b)
140602638349720 # 相同!

>>> a is b
True

Python會對比較小的整數物件進行快取快取起來。當整數比較大的時候就會重新開闢一塊記憶體。

>>> a = 999

>>> b = 999
140602638469952

>>> id(a)
>>> id(b)
>>> a is b

140602638469904 # 不同!
False

這僅僅是在命令列中執行,而在儲存為檔案執行,結果是不一樣的,這是因為直譯器做了一部分優化。

#!/usr/bin/env python

a = 3
b = 3
a = 99999

print(a is b)
b = 99999
True

print(a is b)
結果:
True
[Finished in 0.0s]

這也是為什麼我屢屢用is而不用==,程式執行良好的原因。

總結

1. python中,儘量不要用is, 除非判斷物件是否為None

2. a is b(相同)一定意味著a == b(相等),而a == b(相等) 不一定 a is b (相同)這點比較好理解

3. 如果函式中傳參等,需要引用、拷貝的,注意是否生成了一個新的物件,即使生成了,內部元素是否是同一個物件的引用?尤其注意切片的使用。必要的時候用copy模組進行深拷貝而不要用切片這種淺拷貝形式。

原文釋出時間為:2018-08-12

本文作者:DeepWeaver

本文來自雲棲社群合作伙伴“Python愛好者社群”,瞭解相關資訊可以關注“Python愛好者社群”。


相關文章