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


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

C/C++參數傳遞方式

對於C程序員來說,我們都知道C在函數調用時,采用的是值傳遞,即形參和實參分配不同的內存地址,在調用時將實參的值傳給實參,在這種情況下,在函數內修改形參並不會影響到實參,但是這樣帶來一個問題,如果我們需要刻意地對實參進行修改,就不得不傳遞實參的指針到函數,然后在函數中修改指針指向的數據,以達到修改實參的目的。

后來,C++中引入了引用這個概念,即在函數定義時,在形參前加一個&符號,表示傳遞參數的引用,在寫法上,除了多出一個&符號,其他部分和C中傳值調用一樣,但是實際確是達到了可以在函數內修改實參內容的目的。這種參數傳遞的方式被稱為傳引用。

python的參數傳遞

說完了C/C++的參數傳遞方式,那么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

輸出結果:

[4,5]

可以看到,在上述代碼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時,x的地址剛好從對象1的內存地址變成了對象5的內存地址。

Tips:
    對於C/C++程序員來說,這段代碼並不好理解,變量的值被改變時,竟然是變量的地址變化而不是原變量地址上的值變化!而且,為什么系統會為字面值1和5分配內存空間,這在C/C++中是不存在的!  
    所以我們要從python變量內存角度來理解:對象1和對象5早就在內存中存在,而變量x先是指向1的標簽,在賦值后變成了指向5的標簽。

更多內存細節可以參考我另一篇博客:python變量的內存機制

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,這種機制在一定程度上可以提高資源的重復利用,但是對C/C++程序員來說無疑算是一個陷阱。

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

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

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)
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

從結果可以看出,對於lst列表,僅僅是復制了lst對象,lst[0]僅僅是復制了引用。這其實違背了我們的本意,如果我們要完整地復制整個對象,那又改怎么做呢?

deepcopy

對於上面問題的解決方案是:使用copy模塊的deepcopy方法,相對於copy方法(通常被稱為淺拷貝),deepcopy(通常被稱為深拷貝),淺拷貝就像上述的例子一樣,復制出一個新的對象,但是目標對象中的子對象可能是引用。而deepcopy則是完全復制一個對象的所有內容。

好了,關於python函數調用時參數調用方式的討論就到此為止了,如果朋友們對於這個有什么疑問或者發現有文章中有什么錯誤,歡迎留言

個人郵箱:linux_downey@sina.com
原創博客,轉載請注明出處!

祝各位早日實現項目叢中過,bug不沾身.
(完)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM