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