標籤:

Python 的函數是怎麼傳遞參數的?

比如這段代碼:

def add(num):

num= num+10

d=2

add(d)

print d

輸出 2

如果我要像C那樣傳遞d的地址,使之輸出12,我應該怎麼做?


Python 傳參數可以理解為 C 的 const 指針(your_type* const your_variable),它所指向的對象可以被修改產生副作用,但變數本身不能修改指向其他對象。這個和 C++ 的 reference 差不多。

所以如果一定要產生 C 的修改指針指向其他對象的效果,用 list、dict 或其他自定義的 mutable 對象包裝是一個辦法,但我認為這樣是一種不良實踐。在 C 語言中用參數輸出結果有非常多的理由:

  • C 語言沒有 tuple,不能返回多值,除非聲明一個 struct 類型。這種情況下劃分 in 參數和 out 參數成為一種慣例
  • C 語言沒有異常機制,返回值一般要保留給 errno

但這些情況在 Python 中都是不存在的,所以使用非慣例用法會讓人很「驚訝」,然後這種驚訝就是陷阱了 = =~


首先說說Python中參數傳遞的問題,Python中有可變對象(比如列表List)和不可變對象(比如字元串),在參數傳遞時分為兩種情況:

  1. 對於不可變對象作為函數參數,相當於C系語言的值傳遞;

  2. 對於可變對象作為函數參數,相當於C系語言的引用傳遞。

我們可以分析下樓主的代碼,參數為不可變對象:

def add(num):
num = num + 10
d = 2
add(d)
print d

理所當然的輸出2,為什麼呢?定義了一個變數d,d指向數字2,然後執行add方法,是複製d到num,剛開始num也指向數字2,在函數體內給num+10,整數是不可變對象,所以,將num+10的結果賦值給num變數,此時num指向num+10也就是12,而d本身並沒有發生改變,仍然指向2。

在Python中,對於不可變對象,調用自身的任意方法,並不會改變對象自身的內容,這些方法會創建新的對象並返回,保證了不可變對象本身是永遠不可變的。

當參數為不可變對象時,這裡我們以列表list舉例說明:

如下代碼:

def change(num):
num.append(1)
d = [0]
change(d)
print d

上述代碼的輸出結果為 [0,1],現在來分析原因。執行change()方法時,num指向列表[0],因為列表是可變對象,直接作用在原來list上並不會產生新的對象,所以返回[0,1]。

也就是Python在傳遞參數時在可變對象和不可變對象的傳遞上是有區別的。

回到樓主的問題:

這段代碼

def add(num):
num = num + 10

d=2
add(d)
print d

輸出 2

如果我要像C那樣傳遞d的地址,使之輸出12,我應該怎麼做?

如上所述,我們知道,對於不可變對象,並不會改變對象自身的值,只會創建新的對象,那麼,我們只需要將新創建的對象利用函數返回到d即可,代碼如下:

def add(num):
num = num + 10
return num

d = 2
d = add(d)
print d


如果是從C++轉來的,那麼就可以理解成Python的傳值方式是按照C++中傳指針的方式傳值的,既不是引用也不是值。如果對象是可變的,那麼操作是在傳入對象上操作的,如果是不可變的,那麼相當於這個標識符指向了另一個對象。

其實按照指針的想法來理解就會很簡單。


首先你要明白,Python的函數傳遞方式是賦值,而賦值是通過建立變數與對象的關聯實現的。

對於你的代碼:

  • 執行 d = 2時,你在__main__里創建了d,並讓它指向2這個整型對象。

  • 執行函數add(d)過程中:
    • d被傳遞給add()函數後,在函數內部,num也指向了__main__中的2
    • 但執行num = num + 10之後,新建了對象12,並讓num指向了這個新對象——12。
  • 如果你明白函數中的局部變數與__main__中變數的區別,那麼很顯然,在__main__中,d仍在指著2這個對象,它沒有改變。因此,你列印d時得到了2。

如果你想讓輸出為12,最簡潔的辦法是:

  1. 在函數add()里增加return num
  2. 調用函數時使用d = add(d)

代碼如下:

def add(num):
num += 10
return num

d = 2
d = add(d)
print d


python 是動態束定機制。同一個存儲對象可以束定到一個或者多個標示符上。而標示符沒有類型聲明,因此也是可以束定到任何可束定體上的(包括函數)。

在調用add(num)時,num被束定到了d的束定對象2上,但由於2是不可覆蓋的對象,所以num+10後創建了一個新的對象12並將其束定到num上。隨著函數調用的結束,num就被銷毀了。而此時d仍然束定在對象2上。@十月碼,iOS Developer的回答很好。

也可以用對象

class D:

def __init__(self,x):

self.num=x

def add(self):

self.num+=10

d =D(2)

d.add()


可以查看我寫的這篇文章,可以很清楚的解釋python參數傳遞的問題

Python的函數參數傳遞:傳值?引用?

最近更新了一些內容,希望更加清楚


首先:在Python中所有參數(自變數)在Python里都是按引用傳遞。

數字也是按引用傳遞:

&>&>&> def str_ch(str):

... print id(str)

&>&>&> a = 1

&>&>&> id(a)

41668952

&>&>&> str_ch(a)

41668952

但是對於數字:

&>&>&> a = 1

&>&>&> id(a)

41668952

&>&>&> a += 1

&>&>&> id(a)

41668928

num類型的a的內存地址已經改變(或者說a已經是一個新的對象了)

引起a改變的操作是+=操作,原因是Number數據類型是不允許改變的。

而對於list:

&>&>&> b = [1]

&>&>&> id(b)

140607490691728

&>&>&> b.append(2)

&>&>&> id(b)

140607490691728

list類型的b的內存地址沒有發生改變,append()沒有產生新對象

也就是說在函數調用的時候,函數輸出的a是一個新的對象,函數內的操作並沒有影響到原來的a,只是在函數內產生了一個新的對象a,函數內輸出的是新的a,函數外輸出的仍是舊的a;

而傳遞列表的時候,操作改變了列表的內容,沒有改變地址,也沒有產生新的對象,函數內外輸出的都是同一個對象。


標題名是函數如何傳遞參數,這也是吸引我來看回答的原因,結果沒找到答案。不過看問題描述,涉及的又非傳遞參數這一問題。

首先說明,這個問題相當複雜,絕非隻言片語即可說清楚,需要綜合變數與對象、可變對象與不可變對象、變數與作用域、參數傳遞的知識。

一、關於函數如何傳遞參數

簡單來說,是隱式傳遞。這裡需要明白變數與對象的關係,即python中的變數均沒有類型,對象才有類型,變數不過是指向對象而已。由於這是基礎也是比較重要的知識,篇幅太長,不作展開。

舉例來說,

&>&>&> def fun(a,b):

... a = 10

... b = 20

... print(a,b)

...

&>&>&> fun(1,2)

10 20

fun函數中,a,b均是變數,我們用fun(1,2)傳遞參數時,是隱式地傳遞,等同於a = 1, b = 2這樣的一種賦值方式。為何fun(1,2)的最終結果是10,20而不是1,2,可以看成下面的函數

&>&>&> a,b = 1,2

&>&>&> a = 10

&>&>&> b = 10

&>&>&> print(a,b)

10 10

當理解了是隱匿傳遞後,我們再來理解下下面這個例子,這裡涉及到可變對象與不可變對象的知識點,數字是不可變對象,即你永遠無法直接在原處修改一個不可變變數。

&>&>&> def fun(a):

... a = 99

...

&>&>&> b = 10

&>&>&> fun(b)

&>&>&> print(b)

10

這裡也可以理解為fun(b)中發生了這些事情,1,a=b=10, a=99,print(b),a和b均指向10這個數字對象,a後來改變所指向的對象,指向了99這個數字對象,b依然指向10這個數字對象。結果顯然應該是10。

二、關於如何全局改變,涉及的是變數作用域的知識,

&>&>&> d = 2

&>&>&> def add():

... global d

... d += 10

...

&>&>&> add()

&>&>&> print(d)

12

你舉的這個例子由於涉及到不可變數,所以d的值是無法改變的。如想改變,可在不改變作用域的前提下,使用可變對象,但python不推薦這麼做。如:

&>&>&> def add(num):

... num[0] += 10

...

&>&>&> d = [2]

&>&>&> add(d)

&>&>&> print(d)

[12]

正如前文所說,這個問題還是比較複雜的。


傳值方式是:淺拷貝 #一句話的事


@楊可愛 同學的回答很好,但個人認為還應該補充一種情況:

  • 對於可變對象作為函數參數,且參數不指向其他對象時,相當於C系語言的引用傳遞;否則,若參數指向其他對象,則對參數變數的操作並不影響原變數的對象值

考慮如下情況:

def change(newVar):
newVar = [changedValue]
print("newVar ="+str(newVar))

myVar = [originalValue]
change(myVar)
print("myVar ="+str(myVar))

&>&>&>&>&>&>&>&>&>&>&>&>&>&>
newVar =[changedValue]
myVar =[originalValue]

參數變數newVar指向了新的對象[changedValue],而原變數myVar的值仍為[originalValue]。因為newVar指向了與myVar不同的內存變數,所以對參數變數newVar的操作不影響原變數myVar的值。

可以使用圖表理解:

調用change(myVar)前,myVar指向內存區域Memory_1,該區域值為originalValue:

調用change(myVar)後,myVar指向內存區域仍為Memory_1,該區域值仍為originalValue;newVar指向內存區域Memory_2,該區域值為changedValue:

因為參數變數(newVar)指向了新建的內存空間(Memory_2),所以對參數變數(newVar)的修改([changedValue])不影響原變數(myVar)的指向的內存空間(Memory_1)的值([originalValue])。

這也是因為python的特性" 變數無類型,對象有類型 "。

變數是對內存空間的引用,當參數變數和原變數指向不同的內存空間時,操作互不影響。

總結來說

  1. 對於不可變對象作為函數參數,相當於C系語言的值傳遞;
  2. 對於可變對象作為函數參數,且參數不指向其他對象時,相當於C系語言的引用傳遞。
  3. 對於可變對象作為函數參數,參數指向其他對象,對參數變數的操作不影響原變數的值。


可變對象list,dic都是傳址,不可變對象都是傳值,這樣理解,有毛病嗎?


原話:儘管Python不支持通過引用進行參數傳遞,但通常能通過返回元組,並將結果複製給最初的調用者的參數名來進行模擬

def add(num):

num+=10

return num

d=2

d=add(d)

print d


用函數除了實現功能獨立,還為了重用它。

如果它的參數和你的外部變數相關,那麼如何重用它呢?


我一般是把這個d放到一個list裡面,即[d], 然後在函數裡面修改list[0]~


推薦閱讀:

*吧上有海外留學生問全部用遞歸求第N個質數,不能用循環
Python中 __init__的通俗解釋?
從源碼編譯 Python
Linux線程數限制

TAG:Python |