花了些時間,靜下心來,仔仔細細地研究了一番,總算把Python呼叫函數時引數的傳遞方式給徹底搞清楚了。
為什麼要去計較呼叫函數時,引數到底是怎麼傳遞的呢?這是因為擔心原本好好的變數,在充當引數進入函數之後,會被徹底改造,完全失去原本的面貌;如果不察,錯把馮京當馬涼,以為變數還是原來的樣貌,那可是會出亂子的。例如,假設有這麼個函數
def f(x):那執行
x = new_value
a = value
f(a)
之後,這個a
的值會是什麼呢?是原來的value
嗎?還是變成了new_value
了?這個如果不搞清楚,那又怎麼能確保後續的程式所得到的結果是正確的呢?!
網路上許多文章在討論函數引數的傳遞方式時,都會從call by value和call by reference這兩個方式談起,然後再談到call by sharing這個方式。以前面f(x)
這個函數的例子來看,call by value和call by reference這兩種方式剛好會有相反的結果;一個a
的值還是原來的value
,另一個則是會變成new_value
。但是很不幸的,這兩種方式都不是Python所採用的方式。
有三種方式,其中兩種沒有被採用,那Python用的是call by sharing囉?是有這麼一說,在〈wikipedia: Evaluation strategy〉這篇文章中,就是把Python歸類在使用call by sharing的程式語言類別中;不過在該文中卻也指出,「call by sharing」並非通用的術語,不同的程式語言對這個術語的詮釋方式,並不一致。〈傳值?傳參?〉這篇文章也持相同的觀點。事實上,儘管在Python的官網中找了老半天,也沒看到關於call by sharing這幾個字的任何蛛絲馬跡。
這也不是,那也不是,那Python函數的引數,究竟是用什麼方式傳遞的呢?答案是:call by assignment。在官網的Programming FAQ中有這麼個問題:
How do I write a function with output parameters (call by reference)?
在解答中提到:
Remember that arguments are passed by assignment in Python. Since assignment just creates references to objects, there’s no alias between an argument name in the caller and callee, and so no call-by-reference per se.
所以Python用的是call by assignment無誤。
Python用的是call by assignment,然後呢?當變數充當引數進入函數,在執行完函數之後,這個變數到底有沒有可能會被徹底改造,完全失去原本的面貌?在回答這個問題之前,要先來看看幾段程式,複習一下關於指定運算子(assignment operator),也就是「=」的運作方式。
程式1
a = 8 # 把8指定給a
x = a # 把指定給a的值也指定給x
x = 3 # 讓原本指定給x的值和x脫鉤,並將3指定給x
print(a) # 會印出8
程式2
a = [1, 2, 3] # 把[1, 2, 3]指定給a
x = a # 把指定給a的值也指定給x
x = [5, 7] # 讓原本指定給x的值和x脫鉤,並將[5, 7]指定給x
print(a) # 會印出[1, 2, 3]
程式3
a = [1, 2, 3] # 把[1, 2, 3]指定給a
x = a # 把指定給a的值也指定給x
x[1] = 9 # 修改指定給x的值,把索引值為1的元素改為9
print(a) # 會印出[1, 9, 3]
指定運算子的運作方式,尤其是程式3,是最容易讓人產生混淆的地方;這個部分沒徹底搞清楚,就不算是真正了解指定運算子的運作方式。
搞清楚指定運算子的運作方式之後,要了解call by assignment是怎麼一回事,那就容易多了。來看看下面這幾段程式:
程式4
def f(x):
x = 3 # 把3指定給x
a = 8 # 把8指定給a
f(a) # 把指定給a的值指定給函數的參數,然後執行函數
print(a) # 會印出8
程式5
def f(x):
x = [5, 7] # 把[5, 7]指定給x
a = [1, 2, 3] # 把[1, 2, 3]指定給a
f(a) # 把指定給a的值指定給函數的參數,然後執行函數
print(a) # 會印出[1, 2, 3]
程式6
def f(x):
x[1] = 9 # 修改指定給x的值,把索引值為1的元素改為9
a = [1, 2, 3] # 把[1, 2, 3]指定給a
f(a) # 把指定給a的值指定給函數的參數,然後執行函數
print(a) # 會印出[1, 9, 3]
程式4、5、6其實挺簡單的,就只是把程式1、2、3中的第2、3行改成用函數來做而已。這幾段程式對照著看,應該就可以知道call by assignment是怎麼回事了。
等等!故事還沒結束!如果把程式4、5、6中的變數a
給改成x
,那結果還是一樣嗎?例如,把程式4改成
def f(x):
x = 3
x = 8
f(x)
print(x)
這時候還是會印出8
嗎?答案是:是的,還是會印出8
。這是因為雖然在f(x)
中把3
給指定給x
,但這時候這個x
已經擺脫過去的羈絆,成為全新的、只屬於f(x)
的x
;它已經變成是函數內部的區域變數,再也跟函數外面的x
沒有任何關聯了。程式5的情況也一樣,在函數內部的x被指定新的值[5, 7]
之後,它也變成是區域變數,與函數外面的x
沒有一丁點關係。至於程式6,因為x
在函數內並沒有被指定新的值,所以它還是和原來的值綁在一起,修改某個元素的值,函數外頭的那個x
也會跟著受影響。
針對這個主題,下面這兩篇文章有非常精闢與詳盡的說明,頗值得一讀: