天天看點

python函數調用時--參數傳遞方式

python的參數傳遞

python中參數傳遞到底是傳值還是傳引用呢?

test1.py:      
def test(num):
    num += 10
x = 1
test(x)
print x      

輸出結果:1

test2.py:      
def test(lst):
    lst[0] = 4
    lst[1] = 5
tlist = [1,2]
test(tlist)
print tlist      

在上述代碼test1.py中,在函數中修改傳入的x的值,函數執行完之後,x并沒有改變,至少對于int型變量而言,python函數調用為傳值。

在代碼test2.py中,在函數中修改傳入的tlist的值,函數執行完,list的内容卻被函數修改了,從這裡又可以看出,對于list類型而言,python函數調用為傳引用。

是以,python的函數調用到底是傳值還是傳引用?

python的變量記憶體模型

要搞清楚python的函數調用時傳值還是傳引用,這還得從python的變量記憶體模型說起,作為一個C/C++程式員,對于變量的了解就是CPU會為每個變量配置設定獨立的記憶體空間,在變量生存周期結束時記憶體空間被收回。

但python卻使用了另一種完全不同的機制,對于python而言,一切皆對象,python為每個對象配置設定記憶體空間,但是并非為每個變量配置設定記憶體空間,因為在python中,變量更像是一個标簽,就像在現實生活中,一個人可以有多種身份标簽,比如:XX的父親,XX的兒子,XX的工程師,X地志願者等等,但對應的實體都是同一個人,隻占同一份資源。

x = 1
print(id(x))
print(id(1))
print(id(5))
x= 5 
print(id(x))
print(id(1))
print(id(5))      

輸出:

166656176
166656176
166656128

166656128
166656176
166656128      

當我們将x變量的值由1變成5時(1和5是兩個不同的對象,x變量前後指向了兩個不同對象得位址),x的位址剛好從對象1的記憶體位址變成了對象5的記憶體位址。

python的間接引用機制

可變類型和不可變類型

在python中将類型分為了可變類型和不可變類型,分别有:

可變資料類型:清單,字典   (可變資料類型是某資料在記憶體中修改不産生新的對象,即修改後資料的記憶體位址不變)

不可變資料類型:int、float、string、tuple   (不可變資料類型是指某資料在記憶體中修改産生新的對象,即資料的記憶體位址改變)

簡單地來了解可變類型和不可變類型:在修改該類型變量時是否産生新對象,如果是在原對象上進行修改,為可變對象,如果是産生新的對象,則是不可變對象。

那麼怎麼判斷是否産生新的對象呢?我們可以用python内建id()函數來判斷,這個函數傳回對象在記憶體中的位置,如果記憶體位置有變動,表明變量指向的對象已經被改變。

python傳參時可變類型和不可變類型的差別

事實上,對于python的函數傳遞而言,我們不能簡單地用傳值或者傳址來定義參數傳遞,我們從上一部分中可變類型和不可變類型的角度來分析:

  • 在參數傳遞時,實參将标簽複制給了形參,這個時候形參和實參都是指向同一個對象。
  • 在函數内修改形參(實參傳遞給形參):
  • 對于不可變資料類型變量而言:因為不可變資料類型變量特性,修改變量需要新建立一個對象,形參的标簽轉而指向新對象,而實參沒有變
  • 對于可變資料類型變量而言,因為可變資料類型變量特性,直接在原對象上修改,因為此時形參和實參都是指向同一個對象,是以,實參指向的對象自然就被修改了。

    到這裡,應該就不難了解為什麼在"python的參數傳遞"部分,test1.py和test2.py執行完兩種完全不同的結果了(test1.py傳入不可變類型int,實參未被函數修改。而test2.py傳入可變類型list,實參被修改)。

不僅僅是參數傳遞

在上面我們談論了可變參數和不可變參數在參數傳遞時的行為,其實,這種機制存在于整個python環境中,而不僅僅是參數傳遞中,我們看下面的例子:

list1 = [1,2]
list2 = list1
list1[0] = 3
print list1
print list2      

輸出:

[3, 2]
[3, 2]      

在上述示例中,令list2 = list1,因為根據之前的理論,list2和list1指向同一個對象(指向同一個對象的記憶體位址),是以修改list1時同時也會修改list2,這種機制在一定程度上可以提高資源的重複利用。

可變類型和不可變類型混合的情況

我們人為地将變量分為可變類型和不可變類型,然後分類讨論,以為就萬事大吉了,但是實際情況總是複雜的,我們可以來看看下面的例子:

lst = [(1,2),3]
tup = ([1,2],3)      

像這種情況中,兩種類型糅雜在一起,那怎麼去區分他們到底屬于哪個陣營呢?這樣的變量在修改時會不會建立新的對象呢?我們再來試一試:

tup = ([1,2],3)
tup1 = tup
tup1[0][0] = 3
print tup
print tup1      

輸出結果:

([3,2],3)
([3,2],3)      

這個結果就很有意思了,元組的第一個元素為一個清單,且令tup1 = tup,即兩個變量指向同一個對象,我們修改了元組的第一個元素(清單)的第一個元素,結果兩個元組的資料都變了。由此引出兩個問題:

  • 為什麼元組内的資料可以修改?
  • 作為一個不可變類型變量,為什麼我們修改元組的成員時,不會建立一個新的對象而是在原對象上修改,導緻另一個指向這個對象的變量取值也發生了變化?

對于這個問題,我們可以這樣去了解:在定義tup變量時,此前記憶體中沒有tup對象,是以系統需要建立一個tup對象,對于tup[0]即[1,2],系統會再去找是否存在這樣的對象,如果存在,直接引用原對象,如果不存在,再建立新的對象,對于tup[1]即3,也是同樣的道理。

是以,事實上,tup變量隻是一個引用,tup[0]同時也隻是個引用,tup[1]照樣如此,是以,建立一個符合類型的變量并非我們所想的在記憶體中專門開辟一片區域來放置整個對象,而是在不斷地引用其他對象。

是以,對于問題1,為什麼元組内的資料可以修改?答案是,整個元組并非一個統一的整體,我們修改的是tup[0]的元素,即一個清單變量,自然可以修改,

注意:[0,1]這個整體屬于元組的元素,是不可修改的,即我們不能将[0,1]整體替換成其他,但是單獨的0,1屬于清單的元素,是可以修改的。

對于問題2,既然我們修改的清單的元素,清單是可變參數類型,那麼自然在原對象上修改而非建立新對象。

現實中的各種情況

上面說到了python傳遞參數的特性,那麼如果我們要在函數中修改一個不可變對象的實參,又或者是在函數中不修改可變類型的實參,那該怎麼做呢?

首先,如果要在函數中修改一個不可變參數的實參,最簡單也最實用的辦法就是傳入這個參數同時傳回這個參數,因為雖然是同一個變量,在傳入和傳回時這個變量已經指向了不同的對象:

def test(num):   #num參數指向對象5   
    mum += 10    #num參數指向新對象15
    return num   #傳回num,此時num為15
print test(5)      

然後,如果要在函數中不修改可變參數的實參,這個時候就需要引用另一個子產品:copy,就是重新複制出另一個可變類型參數,我們可以看下面的例子:

import copy
lst = [1,2]
lst_cp = copy.copy(lst) #copy産生出了一個新的對象
print id(lst)
print id(lst_cp)      

輸出結果:

3072211148
3072211340      

從上述示例可以看出,copy過程中系統複制了一個新的對象,而不是簡單地引用原來對象(兩個變量指向資料位址不一樣)。

但是需要注意的是,copy隻對可變類型變量才建立新的對象,而對不可變類型變量,并不建立新的對象,大家可以去試試。

copy複制依然可能存在問題

在可變類型和不可變類型混合的情況下,我們知道了,一個變量很可能并非僅僅指向一個完整的對象,變量的子元素依然存在引用的情況,比如在lst = [[1,2],3)]中,lst[0]就是引用了别處的對象,而非在記憶體中完全存在一個單獨的[[1,2],3]對象,那麼,如果使用copy對lst進行複制,對于lst[0],是僅僅複制了引用,還是複制了整個對象呢?我們可以看下面的例子:

import copy
lst = [[1,2],3]
lst_cp = copy.copy(lst)
print id(lst)
print id(lst_cp)
print id(lst[0])
print id(lst_cp[0])      

輸出結果:

3072042988
3072043404
3072043020
3072043020      

deepcopy