python中的引用傳遞
首先必須理解的是,python中一切的傳遞都是引用(地址),無論是賦值還是函數調用,不存在值傳遞。
可變對象和不可變對象
python變量保存的是對象的引用,這個引用指向堆內存里的對象,在堆中分配的對象分為兩類,一類是可變對象,一類是不可變對象。不可變對象的內容不可改變,保證了數據的不可修改(安全,防止出錯),同時可以使得在多線程讀取的時候不需要加鎖。
不可變對象(變量指向的內存的中的值不能夠被改變)
當更改該對象時,由於所指向的內存中的值不可改變,所以會把原來的值復制到新的空間,然后變量指向這個新的地址。
python中數值類型(int和float),布爾型bool,字符串str,元組tuple都是不可變對象。
a = 1
print id(a) # 40133000L,整數1放在了地址為40133000L的內存中,a變量指向這個地址。
a += 1
print id(a) # 40132976L,整數int不可改變,開辟新空間存放加1后的int,a指向這個新空間。
可變對象(變量指向的內存的中的值能夠被改變)
當更改該對象時,所指向的內存中的值直接改變,沒有發生復制行為。
python中列表list,字典dict,集合set都是可變對象。包括自定義的類對象也是可變對象。
a = [1,2,3]
print id(a) # 44186120L。
a += [4,5] # 相當於調用了a.extend([4,5])
print id(a) # 44186120L,列表list可改變,直接改變指向的內存中的值,沒開辟新空間。
a = a + [7,8] # 直接+和+=並不等價,使用+來操作list時,得到的是新的list,不指向原空間。
print id(a) # 44210632L
def f(default_arg=[]):
default_arg.append('huihui')
f() # ['huihui']
f() # ['huihui', 'huihui'] # 函數默認的可變參數並不會每次重新初始化,而是使用上次的作為默認值。
f([]) # ['huihui'] # 自行傳入參數即可。
def f(default_arg=None): # 一個常見的做法是判斷是否為空,為空則新建list,否則append
if default_arg is None:
default_arg = []
default_arg.append("some_string")
return default_arg
可變對象和不可變對象
() is () # 返回True,因為tuple是不可變對象(不可改變,怎么定義都一樣)
'' is '' # 返回True,因為str是不可變對象
None is None # 返回True,None也是不可變的
[] is [] # 返回False,因為是可變對象(可能改變,定義出來的兩個必然要不一樣)
{} is {} # 返回False,因為是可變對象
[] == [] # 返回True,注意==和is的不同,==只比較內容,is比較地址(id)
class Student:
pass
Student() is Student() # 返回False,自定義類型也是可變對象,兩次定義的對象地址是不同的
id(Student()) == id(Student()) # 返回True,這里比較神奇,是因為創建一個Student對象,id()后返回地址但是進行了對象銷毀,第二次又重新創建,兩次占用了同一個地址
不可變對象的編譯時駐留(類似java的常量池)
int的駐留:-5到256之間的整數都會進行駐留,再次定義的變量地址不變,為什么是-5到256呢,這是解釋器決定的,依賴於具體實現。
str的駐留:只包含字母,數字,下划線的字符串會駐留;長度為0或1的會駐留;
a = -5
b = -5
a is b # True,-5到256之間的整數,駐留(直覺上這部分數據會頻繁調用,駐留可以節省資源)
a = 256
b = 256
a is b # True,-5到256之間的整數,駐留
a = -6
b = -6
a is b # False,非-5到256之間的整數,不駐留
a = 257
b = 257
a is b # False,非-5到256之間的整數,不駐留
a = 'hello_world'
b = 'hello'+'_'+'world'
a is b # True,只包含字母,數字,下划線的字符串會駐留
a = 'hello_world!'
b = 'hello_world!'
a is b # False,包含了特殊字符!, 不駐留
'hello_world' is '_'.join(['hello', 'world']) # False,因為駐留是編譯階段發生的,join在解釋階段才產生結果,未進行駐留
a, b = 'hello_world!', 'hello_world!'
a is b # True 編譯器的優化,在同一行賦值字符串時,只創建一個對象,指給兩個引用。(ps:不適用3.7.x版本,3.7.x中會返回False)
關於駐留的陷阱
跟駐留沒有直接關系(霧?),是在命令行運行和py文件直接運行有一些差異。先看之前的小例子。
a = 257
b = 257
a is b # False,非-5到256之間的整數,不駐留。
事實上,在命令行運行得到的才是False(我做的小實驗一般都在交互式命令行上運行)
如果把這三行放到py文件里,再直接運行,得到的是True,因為py文件是一次性編譯的,而交互式命令行按一行為單位(嚴格說是命令結束時的全部,因為會有for while這種)編譯
或者在交互式中把這三行定義為函數,再調用函數,返回也是True
def func():
a = 257
b = 257
return a is b
func() # 返回True
這是由python的代碼塊機制導致的,在同一代碼塊中相同值的賦值會指向同一個對象。函數體,類對象體,py文件,模塊都可以看作一個代碼塊。
在交互式命令行上,一行看作一個代碼塊(嚴格說是命令結束時的全部,因為會有for while這種),所以,這里所謂“代碼塊的優化”,就是前面提到的,同行賦值的優化,只在一行(代碼塊)上優化。
到具體直接運行py文件,又有了更大范圍的代碼塊的優化,所以連着兩行相同賦值的對象,會指向同一個對象。
引用傳遞后的改變
a = [1,2,3]
b = a
b[0] = 2 # 由於list是可變對象,改變b時候會導致a的改變,a和b都是[2,2,3]
s = 'abc'
s2 = s
s2 += 'd' # 由於str是不可變對象,s2是新建的對象,s2的修改不會影響s。s為'abc',s2為'abcd'。
list注意點
a = [1,2,3]
b = a
a is b # True,因為按引用傳遞,a和b存的地址(引用)是一樣的,改變b相當於改變a。
b = a[:]
a is b # False,想使用list的值卻不想修改原list時可以使用切片[:]拷貝一份到新空間。
a = [1,2,3]
id(a) # 140376329323528
a = [1,2,3]
id(a) # 140376359286920,兩次定義相同的list,但是其地址並不相同,會創造新對象
a = [1,2,3]
id(a) # 140376329323528
a[:] = [1,2,3]
id(a) # 140376329323528,因為a[:]切片創建的是新空間,對新空間賦值不影響舊空間a,所以a的地址跟原來一致。
a =[ [0]*2 ]* 2 # 以這種方式創建一個二維list,此時a為[[0,0],[0,0]]。
a[0] is a[1] # True,這種創建方法的機制是復制list,所以2個list其實是同一個list。
a[0][0] = 1 # 改變第一個list時第二個list也改變,此時a為[[1,0],[1,0]]。
a[0] += [1] # 改變第一個list時第二個list也改變,此時a為[[1,0,1],[1,0,1]]。+=相當於extend,對list進行原地修改。
a[0] = a[0] + [1] # 改變第一個list時,第二個list不改變,此時a為[[1,0,1,1],[1,0,1]]。因為不是原地改變,而是創建了新list,然后給原來的引用賦了新值。
a[0] = [1,2] # a[0]指向創建的新list[1,2]。此時a[1]不變,a為[[1,2],[1,0,1]]。同樣是給a[0]賦值了新的list[1,2],不會影響到a[1]。
a = [[0]*2 for _ in range(2)] # 相對正確的創建方式,這樣創建的二維list,改變a[0]並不會影響a[1]
a[0] is a[1] # False
a = [ []*1000 ] # 同理,這么定義返回的是[],並不能得到含有1000個空list的list(直覺誤區)
a = [ [] for _ in range(1000) ] # 正確的定義方式
x = float('nan')
x == x, [x] == [x] # False, True 因為list之間比較的時候先比較元素的地址,如果相等則認為相等,當id不相等時才比較值
