Python/ target=_blank class=infotextkey>Python就是把一些參數(shù)從一個(gè)函數(shù)傳遞到另一個(gè)函數(shù),從而使其執(zhí)行相應(yīng)的任務(wù)。但是你有沒有想過,參數(shù)傳遞的底層是如何工作的,原理又是怎樣的呢?
實(shí)際工作中,很多人會(huì)遇到這樣的場(chǎng)景:寫完了代碼,一測(cè)試,發(fā)現(xiàn)結(jié)果和自己期望的不一樣,于是開始一層層地 debug。花了很多時(shí)間,可到最后才發(fā)現(xiàn),是傳參過程中數(shù)據(jù)結(jié)構(gòu)的改變,導(dǎo)致了程序的“出錯(cuò)”。
比如,我將一個(gè)列表作為參數(shù)傳入另一個(gè)函數(shù),期望列表在函數(shù)運(yùn)行結(jié)束后不變,但是往往“事與愿違”,由于某些操作,它的值改變了,那就很有可能帶來后續(xù)程序一系列的錯(cuò)誤。
因此,了解 Python 中參數(shù)的傳遞機(jī)制,具有十分重要的意義,這往往能讓我們寫代碼時(shí)少犯錯(cuò)誤,提高效率。今天我們就一起來學(xué)習(xí)一下,Python 中參數(shù)是如何傳遞的。
什么是值傳遞和引用傳遞
如果你接觸過其他的編程語(yǔ)言,比如 C/C++,很容易想到,常見的參數(shù)傳遞有 2 種:值傳遞和引用傳遞。所謂值傳遞,通常就是拷貝參數(shù)的值,然后傳遞給函數(shù)里的新變量。這樣,原變量和新變量之間互相獨(dú)立,互不影響。比如,我們來看下面的一段 C++ 代碼:
#include <IOStream>
using namespace std;
// 交換2個(gè)變量的值
void swap(int x, int y) {
int temp;
temp = x; // 交換x和y的值
x = y;
y = temp;
return;
}
int main () {
int a = 1;
int b = 2;
cout << "Before swap, value of a :" << a << endl;
cout << "Before swap, value of b :" << b << endl;
swap(a, b);
cout << "After swap, value of a :" << a << endl;
cout << "After swap, value of b :" << b << endl;
return 0;
}
Before swap, value of a :1
Before swap, value of b :2
After swap, value of a :1
After swap, value of b :2
這里的 swap() 函數(shù),把 a 和 b 的值拷貝給了 x 和 y,然后再交換 x 和 y 的值。這樣一來,x 和 y 的值發(fā)生了改變,但是 a 和 b 不受其影響,所以值不變。這種方式,就是我們所說的值傳遞。
所謂引用傳遞,通常是指把參數(shù)的引用傳給新的變量,這樣,原變量和新變量就會(huì)指向同一塊內(nèi)存地址。如果改變了其中任何一個(gè)變量的值,那么另外一個(gè)變量也會(huì)相應(yīng)地隨之改變。
還是拿我們剛剛講到的 C++ 代碼為例,上述例子中的 swap() 函數(shù),如果改成下面的形式,聲明引用類型的參數(shù)變量:
void swap(int& x, int& y) {
int temp;
temp = x; // 交換x和y的值
x = y;
y = temp;
return;
}
那么輸出的便是另一個(gè)結(jié)果:
Before swap, value of a :1
Before swap, value of b :2
After swap, value of a :2
After swap, value of b :1
原變量 a 和 b 的值被交換了,因?yàn)橐脗鬟f使得 a 和 x,b 和 y 一模一樣,對(duì) x 和 y 的任何改變必然導(dǎo)致了 a 和 b 的相應(yīng)改變。
不過,這是 C/C++ 語(yǔ)言中的特點(diǎn)。那么 Python 中,參數(shù)傳遞到底是如何進(jìn)行的呢?它們到底屬于值傳遞、引用傳遞,還是其他呢?
在回答這個(gè)問題之前,讓我們先來了解一下,Python 變量和賦值的基本原理。
a = 1
b = a
a = a + 1
這里首先將 1 賦值于 a,即 a 指向了 1 這個(gè)對(duì)象,如下面的流程圖所示:
接著 b = a 則表示,讓變量 b 也同時(shí)指向 1 這個(gè)對(duì)象。這里要注意,Python 里的對(duì)象可以被多個(gè)變量所指向或引用。
最后執(zhí)行 a = a + 1。需要注意的是,Python 的數(shù)據(jù)類型,例如整型(int)、字符串(string)等等,是不可變的。所以,a = a + 1,并不是讓 a 的值增加 1,而是表示重新創(chuàng)建了一個(gè)新的值為 2 的對(duì)象,并讓 a 指向它。但是 b 仍然不變,仍然指向 1 這個(gè)對(duì)象。
因此,最后的結(jié)果是,a 的值變成了 2,而 b 的值不變?nèi)匀皇?1。
通過這個(gè)例子你可以看到,這里的 a 和 b,開始只是兩個(gè)指向同一個(gè)對(duì)象的變量而已,或者你也可以把它們想象成同一個(gè)對(duì)象的兩個(gè)名字。簡(jiǎn)單的賦值 b = a,并不表示重新創(chuàng)建了新對(duì)象,只是讓同一個(gè)對(duì)象被多個(gè)變量指向或引用。
同時(shí),指向同一個(gè)對(duì)象,也并不意味著兩個(gè)變量就被綁定到了一起。如果你給其中一個(gè)變量重新賦值,并不會(huì)影響其他變量的值。
明白了這個(gè)基本的變量賦值例子,我們?cè)賮砜匆粋€(gè)列表的例子:
l1 = [1, 2, 3]
l2 = l1
l1.Append(4)
l1
[1, 2, 3, 4]
l2
[1, 2, 3, 4]
同樣的,我們首先讓列表 l1 和 l2 同時(shí)指向了[1, 2, 3]這個(gè)對(duì)象。
由于列表是可變的,所以 l1.append(4) 不會(huì)創(chuàng)建新的列表,只是在原列表的末尾插入了元素 4,變成[1, 2, 3, 4]。由于 l1 和 l2 同時(shí)指向這個(gè)列表,所以列表的變化會(huì)同時(shí)反映在 l1 和 l2 這兩個(gè)變量上,那么,l1 和 l2 的值就同時(shí)變?yōu)榱薣1, 2, 3, 4]。
另外,需要注意的是,Python 里的變量可以被刪除,但是對(duì)象無(wú)法被刪除。比如下面的代碼:
arr = [1, 2, 3]
del arr
del arr 刪除了 arr 這個(gè)變量,從此以后你無(wú)法訪問 arr,但是對(duì)象[1, 2, 3]仍然存在。Python 程序運(yùn)行時(shí),其自帶的垃圾回收系統(tǒng)會(huì)跟蹤每個(gè)對(duì)象的引用。如果[1, 2, 3]除了 arr 外,還在其他地方被引用,那就不會(huì)被回收,反之則會(huì)被回收。
由此可見,在 Python 中:
1、變量的賦值,只是表示讓變量指向了某個(gè)對(duì)象,并不表示拷貝對(duì)象給變量;而一個(gè)對(duì)象,可以被多個(gè)變量所指向。
2、可變對(duì)象(列表,字典,集合等等)的改變,會(huì)影響所有指向該對(duì)象的變量。
3、對(duì)于不可變對(duì)象(字符串、整型、元組等等),所有指向該對(duì)象的變量的值總是一樣的,也不會(huì)改變。但是通過某些操作(+= 等等)更新不可變對(duì)象的值時(shí),會(huì)返回一個(gè)新的對(duì)象。
4、變量可以被刪除,但是對(duì)象無(wú)法被刪除。
Python 函數(shù)的參數(shù)傳遞
從上述 Python 變量的命名與賦值的原理講解中,相信你能舉一反三,大概猜出 Python 函數(shù)中參數(shù)是如何傳遞了吧?
這里首先引用 Python 官方文檔中的一段說明:
“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.”
準(zhǔn)確地說,Python 的參數(shù)傳遞是賦值傳遞 (pass by assignment),或者叫作對(duì)象的引用傳遞(pass by object reference)。Python 里所有的數(shù)據(jù)類型都是對(duì)象,所以參數(shù)傳遞時(shí),只是讓新變量與原變量指向相同的對(duì)象而已,并不存在值傳遞或是引用傳遞一說。比如,我們來看下面這個(gè)例子:
def my_func1(b):
b = 2
a = 1
my_func1(a)
a
1
這里的參數(shù)傳遞,使變量 a 和 b 同時(shí)指向了 1 這個(gè)對(duì)象。但當(dāng)我們執(zhí)行到 b = 2 時(shí),系統(tǒng)會(huì)重新創(chuàng)建一個(gè)值為 2 的新對(duì)象,并讓 b 指向它;而 a 仍然指向 1 這個(gè)對(duì)象。所以,a 的值不變,仍然為 1。
那么對(duì)于上述例子的情況,是不是就沒有辦法改變 a 的值了呢?答案當(dāng)然是否定的,我們只需稍作改變,讓函數(shù)返回新變量,賦給 a。這樣,a 就指向了一個(gè)新的值為 2 的對(duì)象,a 的值也因此變?yōu)?2。
def my_func2(b):
b = 2
return b
a = 1
a = my_func2(a)
a
2
不過,當(dāng)可變對(duì)象當(dāng)作參數(shù)傳入函數(shù)里的時(shí)候,改變可變對(duì)象的值,就會(huì)影響所有指向它的變量。比如下面的例子:
def my_func3(l2):
l2.append(4)
l1 = [1, 2, 3]
my_func3(l1)
l1
[1, 2, 3, 4]
這里 l1 和 l2 先是同時(shí)指向值為[1, 2, 3]的列表。不過,由于列表可變,執(zhí)行 append() 函數(shù),對(duì)其末尾加入新元素 4 時(shí),變量 l1 和 l2 的值也都隨之改變了。但是,下面這個(gè)例子,看似都是給列表增加了一個(gè)新元素,卻得到了明顯不同的結(jié)果。
def my_func4(l2):
l2 = l2 + [4]
l1 = [1, 2, 3]
my_func4(l1)
l1
[1, 2, 3]
為什么 l1 仍然是[1, 2, 3],而不是[1, 2, 3, 4]呢?
要注意,這里 l2 = l2 + [4],表示創(chuàng)建了一個(gè)“末尾加入元素 4“的新列表,并讓 l2 指向這個(gè)新的對(duì)象。這個(gè)過程與 l1 無(wú)關(guān),因此 l1 的值不變。當(dāng)然,同樣的,如果要改變 l1 的值,我們就得讓上述函數(shù)返回一個(gè)新列表,再賦予 l1 即可:
def my_func5(l2):
l2 = l2 + [4]
return l2
l1 = [1, 2, 3]
l1 = my_func5(l1)
l1
[1, 2, 3, 4]
這里你尤其要記住的是,改變變量和重新賦值的區(qū)別:
1、my_func3() 中單純地改變了對(duì)象的值,因此函數(shù)返回后,所有指向該對(duì)象的變量都會(huì)被改變;
2、但 my_func4() 中則創(chuàng)建了新的對(duì)象,并賦值給一個(gè)本地變量,因此原變量仍然不變。
至于 my_func3() 和 my_func5() 的用法,兩者雖然寫法不同,但實(shí)現(xiàn)的功能一致。不過,在實(shí)際工作應(yīng)用中,我們往往傾向于類似 my_func5() 的寫法,添加返回語(yǔ)句。這樣更簡(jiǎn)潔明了,不易出錯(cuò)。
總結(jié)
今天,我們一起學(xué)習(xí)了 Python 的變量及其賦值的基本原理,并且解釋了 Python 中參數(shù)是如何傳遞的。和其他語(yǔ)言不同的是,Python 中參數(shù)的傳遞既不是值傳遞,也不是引用傳遞,而是賦值傳遞,或者是叫對(duì)象的引用傳遞。
需要注意的是,這里的賦值或?qū)ο蟮囊脗鬟f,不是指向一個(gè)具體的內(nèi)存地址,而是指向一個(gè)具體的對(duì)象。
1、如果對(duì)象是可變的,當(dāng)其改變時(shí),所有指向這個(gè)對(duì)象的變量都會(huì)改變。
2、如果對(duì)象不可變,簡(jiǎn)單的賦值只能改變其中一個(gè)變量的值,其余變量則不受影響。
清楚了這一點(diǎn),如果你想通過一個(gè)函數(shù)來改變某個(gè)變量的值,通常有兩種方法。一種是直接將可變數(shù)據(jù)類型(比如列表,字典,集合)當(dāng)作參數(shù)傳入,直接在其上修改;第二種則是創(chuàng)建一個(gè)新變量,來保存修改后的值,然后將其返回給原變量。在實(shí)際工作中,我們更傾向于使用后者,因?yàn)槠浔磉_(dá)清晰明了,不易出錯(cuò)。






