關於Python中參數傳遞和作用域的問題?

題主最近在入門Python,有幾點疑惑:

1 將一個存儲了字元串的變數a傳遞給另一個變數b,這在內存中是怎麼樣的?

2 為什麼列表這些傳遞的是引用?那列表作為參數傳遞給函數不就是突破了函數的局部作用域了嘛?

說一下我的疑惑,1看起來是像傳遞一個值,而2像是傳遞一個地址,這兩種方式能在一個語言中並存嘛?

補充:題主沒有C/C++基礎,僅有自學的JAVA,請大家不吝賜教,希望能詳細點,另外如果能給出參考資料也蠻好的~另外求關於作用域,名稱空間,Python面向對象編程等方面的參考資料,感覺教材講的不透徹。


對於Python而言,實際上參數傳遞的是值,但是,這個「值」是對對象的引用。

Python里的所有東西都是對象,無論list還是dict還是簡單的int、str、float都是對象,所有的「變數」都是對對象的引用,因此Python嚴格的說不存在「賦值」,只存在對象到名字的綁定。

參數傳遞時會傳遞這個引用並複製,綁定到以函數內為作用域的一個局部名字。如果對參數「賦值」,改變的只是這個局部名字的綁定,不會改變原本傳入參數在外部的值;但是如果對這個對象調用某些方法產生了變化,由於和外部引用的是同一個對象,因此對外部而言這個對象也改變了。這就是為什麼你對參數里的list進行apped會把結果帶到外部,但重新「賦值」之後再做任何操作就都不會影響外面原本的list產生任何影響了。

這個邏輯貫穿整個Python,所謂Python的閉包和全局變數也是如此,對用到的外部名字都會複製引用,因此訪問是沒問題的,但若「賦值」則會變成指向新對象。只有對保留了nonlocal/global的名字才會把原本的名字本身帶過來,這樣才實現全局變數。

這一個參數傳遞機制其實和Java是差不多的,但不同的是Java里還保留著簡單變數,在Python里則不存在「簡單變數」這種東西。其實Python里的不少設計都與Java很類似,例如str這個東西並不是容器而是個不可變對象,任何str操作都會返回新的str對象,這一點和Java的String是基本一致的,但是卻與字元數組不同。


想要了解 Python 的參數是如何傳遞的,首先要知道 python 的變數的內存管理機制

不同的編程語言的內存分配策略有時是大相徑庭,在 C 語言中分配內存時:

int a = 1;

現在想像下面這種情形:將 1 放入一個變數名為 a 的水杯中,這個水杯作為 1 的載體。上面這句語句就是創建出一塊內存區域(a)來存儲變數(水杯)的值(1)。

a = 2;

如果想要改變水杯中的值,只需要將想要修改的值(2)直接放入水杯中替換之前的1就完成了重新賦值。

int b = a;

將一個變數的值賦予其他的變數則會開闢新的存儲空間,並複製當前水杯中的值,然後放入新的水杯。

---------------------------

而在 Python 中變數的內存分配很詭異(至少對於我來說),變數不再是水杯了,而更像一個標籤。在 Python 中分配內存時:

a = 1

Python 這時將一個名為 a 的標籤綁定到變數值 1 上。

a = 2

正如你所看到的,在修改變數值時,標籤 a 是直接將自己綁定到內存中的值 2 上。

b = a

如果增加另一個變數 b 的值也為 2,標籤 b 也會像 a 一樣一起綁定到數值 2 上。

---------------------------

現在就可以講講參數傳遞的問題:

Python 中姑且可以說有兩種函數變數傳遞的方式,一種稱為 Call-by-value,另一種是 Call-by-reference(就是題主說的引用傳遞)。

  • 首先說說 call-by-value,在 call-by-value 中,參數表達式會被 Python 解釋並綁定到函數中對應的變數中。所以如果參數表達式是一個變數,在函數中則會複製該變數的值然後再使用這個複製的值。因此這個變數值在函數外部作用域完全不會被改變。
  • 在 call-by-reference 中:函數會直接使用一個值的隱含式的引用,而不是像 call-by-value 直接使用另外複製出的值,這樣做的後果就是,在函數內部修改同名變數的值時導致外部的同名變數跟著改變。但這個傳遞方式也有很突出的優點:不論在時間還是內存空間效率都很高,因為不用另外複製變數。

注意:嚴格意義上來說 Python 傳參策略既不是 call-by-reference,也不是 call-by-value,而是另一種機制 call-by-object,亦 call-by-Object-reference ,或 call-by-sharing(參數傳遞是對一個對象的引用,但是這個引用卻又是 passed by value)。根據不同的情況使用不同的機制,所以只是在這裡用這兩種方式來進行更確切地說明。

---------------------------

接下來看幾個具體的示例:

下面是一個很讓人費解的示例:

def a(the_list):
print(Got, the_list)
the_list.append(treats)
print(Set to, the_list)

outer_list = [Dogs, eats]

print(Before, outer_list = , outer_list)
a(outer_list)
print(After, outer_list = , outer_list)

# Outputs in terminal
# &>&>&> Before, outer_list = [Dogs, eats]
# &>&>&> Got [Dogs, eats]
# &>&>&> Set to [Dogs, eats, treats]
# &>&>&> After, outer_list = [Dogs, eats, treats]

為什麼會這樣?因為 `the_list` 就是對 `list[Dogs, eats]` 的引用,而不是複製。並且在 Python 中 Object 對象是 mutable 可變對象(String等為不可變對象),所以 `append()` 方法能夠改變 `the_list` 中的值。

理解上面的示例後,看下面更進階的一個示例:

def b(the_list):
print(Got, the_list)
the_list = [You, never, lie]
print(Set to, the_list)

outer_list = [Dogs, eats]

print(Before, outer_list = , outer_list)
a(outer_list)
print(After, outer_list = , outer_list)

# Outputs in terminal
# &>&>&> Before, outer_list = [Dogs, eats]
# &>&>&> Got [Dogs, eats]
# &>&>&> Set to [You, never, lie]
# &>&>&> After, outer_list = [Dogs, eats]

為什麼在這個例子中之前所說的不管用了呢?the_list 剛進入函數時,確實是對變數的引用,但是當 the_list = [You, never, lie] 這句語句執行之後,相當於直接在函數中又創建了一個新的局部變數,名字也叫 the_list, 本質上已經不同於參數 the_list 。因為它將 the_list 綁定到了 list[You, never, lie] 上(想想之前所說的變數內存分配機製圖,the_list 就像一個標籤,直接綁定到了list[You, never, lie] 上),所以自然也就沒有改變 outer_list 的值.

希望以上的回答能夠解決題主對參數傳遞的疑惑。如有認知錯誤,歡迎指出。

Reference: http://leone.wang/2017/02/20/memory-management-of-variables-in-python/


作用域是針對變數的,不是針對對象的,把a傳遞給函數參數b,a這個「變數」的作用域沒有變化,而b這個「變數」有它自己的作用域,儘管他倆是引用同一個對象

py中任何傳遞和賦值都是引用傳遞,不存在「值傳遞」一說,題主舉的字元串的例子看起來「像」值傳遞是因為字元串不可修改,你也沒法通過代碼層面去證明是引用或值傳遞,效果一樣,但是底層實現是引用傳遞的


這個問題我們可以換個角度看,看看python的C-API,用c寫的python模塊中參數傳進去的都是一個PyObject的指針。比如這個例子,拋出一個ValueError異常

#include "Python.h"

static PyObject* raiseError(PyObject* self, PyObject* args)
{
PyErr_SetString(PyExc_ValueError, "Ooops");
return NULL;
}

可以看出,無論是傳入參數還是傳出的返回值,都是一個PyObject的結構體的指針。這個C函數在python中被調用的時候,結構體被重新構造為python里的函數。這在python這種一切皆對象的語言中,應該叫傳對象的引用更為合適,而且,在各種C介面的參數中,指針大量存在,一個合格的c介面的python模塊可能並不存在傳值這種東西。

因此,python中的參數傳遞,我認為都應該歸為傳遞對象的引用這一方式,無論是不可變的str,int等類型還是可變的list、dict類型。

回到問題,

存儲了字元串的變數a傳遞給另一個變數b

a和b保存的都是不可變的字元串變數的引用。

那列表作為參數傳遞給函數不就是突破了函數的局部作用域了嘛?

沒明白你這個是要表達什麼意思,在py2的閉包中會有一個技巧是用list傳遞來突破作用域,py3有nolocal了就不用了。


你這些問題需要別的知識,你不學,是不能理解的


推薦閱讀:

Day 4-6, 列印、文件、函數
如何用python解析json對象(基礎篇)
[新聞] CPython / 微軟 Pyjion / IBM Python+OMR
Flask框架從入門到實戰
python編程基礎(一)

TAG:編程語言 | Python |