Python 是 Pass By Value, Pass by Reference, 還是 Pass by Sharing?
同步刊登於 Medium,文章連結。
常見的語言的求值策略(Evaluation strategy)大概離不開這幾個,例如:JavaScript (Call by Value, Call by Sharing)、Java (Call by Value)、C (Call by Value)、C++ (Call by Value, Call by Reference)
這次使用使用淺顯易懂的語言,並搭配圖片來說明到底 Python 中是 Pass by Value, Pass by Reference, 還是 Pass Sharing?
用 C++ 理解 Pass by Value
C++ 是探討 Pass by Value 與 Pass by Reference 的最佳選擇。我們先來理解 Pass by Value。
Pass by Value (Output)
執行結果
1 |
|
Pass by Value (Address)
可以看到函式內部的數並不會受到影響,遵守作用域( Scope)範圍。我們再執行另一個程式,查看記憶體位址的改變。
執行結果:
1 |
|
可以發現 num 的位址遵守作用域 (Scope),經過這兩個程式之後,可以理解到 Pass by Value 是在呼叫函式的時候,複製一份引數(number)給函式使用。
Step 1:宣告 number 為 7
Step 2:呼叫函式時,複製一份給 number 的值複製一份給 num。
step 3:修改 num 的值時,修改的是複製出來的那一份。因此原本的 number 不受影響。
用 C++ 理解 Pass by Reference
Pass by Reference
執行結果:
1 |
|
Pass by Reference (Address)
跟剛剛不同的是,因為 Pass by Reference 傳的是參照,也就是地址,所以裡外等同於是一個變數。接著我們一樣把記憶體位址印出來。看看是不是真的是同樣的記憶體位址。
執行結果:
1 |
|
發現結果如我們推測的,從頭到尾記憶體位址都是 0x7ffd45470b64
。因為我們傳的是記憶體位址嘛。
Step 1:宣告 number
Step 2:呼叫 passByReference 時不會複製一份 number 的值,也就是「共用」同一份的值。
Step 3:因為共用一份資料,num 更新時,也更新到了 number。
Step 4: 看所以最後得到的結果是 After: number is 3
,因為 number 已經被改變
來看看 Python 是不是 Pass by Value
接著我們來看看 Python 是否是 Pass by Value。
執行結果
1 |
|
執行完 function_1 後,發現原本的 number,並沒有因為在函式中重新指派而改變成 1,還是維持原本的 7。
看起來跟 C++ 的 Pass Value 很像,接著我們再做一個實驗,把記憶體位置印出來。
執行結果:
1 |
|
可以發現,行為雖然像是 Pass by Value,但是記憶體位址變化的時候,並不是執行 function_1 函式的當下,而是在指派 num = 1 之後。這是為什麼呢?
Step 1:將 7 指派給變數 number,此時 7 存到記憶體中。
Step 2:呼叫函式 pass_by_value() 的時候,將 7 傳了進去。此時函式並不會複製一份值進去。
Step 3:當執行到 num=1
時, num 是整數型別,是 Imuttable Object 不可變物件,值無法改變,因此會新增一塊記憶體來儲存 num。
Step 4 : 所以最後印出時,不會是 1,而是 7 ,因為 number 與 num 已經沒有共用記憶體位置
結論:Python 的 function_1 跟 C++的 Call by Value 最後的結果都有分開的記憶體位址。但是從記憶體位址變化的時機可以看得出來,讓 Python 有此現象的,是 Imuttable Object 不可變的特性,而非 Call by Value。
來看看 Python 是不是Pass By Reference
接著我們來看看 Python 是否是 Pass by Reference。
執行結果
1 |
|
哇,外面 object 的內容居然被改變了,跟剛剛的 number 行為完全不一樣,這是怎麼回事?難道這是 Call by Reference? 可是我們沒有傳地址進去呀。
一樣,我們先把記憶體位址印出來試試看。
執行結果:
1 |
|
記憶體位址完全沒有改變,這是為什麼呢?因為剛剛我們使用字典 (dict) 這種資料型別,是屬於可變物件 (Mutable Object),內容可以改變
深入資料型別字典(dict) 的記憶體調用
我們來看看這段程式記憶體的調用。這次的圖表將記憶體更精確的分為儲存空間所儲存的「資料 Data」與儲存空間的「地址 Address」。
因為字典這種物件是 mutable 可變的,代表這種物件的值是可以改變的,所以想要來看看到底是記憶體怎麼儲存,才是可以改變的
如果不了解 Imuttable 的人可以參考我寫的另外一篇文章。Python — Mutable vs Immutable。裡面講解了 Python 中可變與不可變的概念。
宣告 object 物件的時候,變數 Variable: object,指向一個記憶體位址為 x8
資料為 x7
。變數本身不直接儲存資料的,只會儲存資料的記憶體位址。而真正的資料儲存在地址為 x7
資料為 {‘name’: ‘Leo’, ‘age’: 25}
。
是呼叫function_2 函式的時候,object 的參照傳進去函式裡面,變數名稱變為 obj 。 變數 obj 將內容的地址,儲存在 x9 記憶體位址,資料為 x7。告訴變數 obj 可以去地址 x7 拿資料,這樣就會找到原本物件內容了。
接著實際執行 pass_by_reference() 函式,因為之前變數 obj 將資料指向跟變數 dict_object 相同的物件,所以實際修改到的資料,是原本的資料。最後印出來的時候發現,原始的物件內容已經被改變。
回顧一下 function_2 的執行結果,函式內部的改變會改變 object 外部的內容。
執行結果:
1 |
|
來看看 Python 是不是 Pass by Sharing
接下來我們來看範例 3,將範例 2 稍作改變,來看看會發生什麼事情
我們將 function_2() 方法改成 function_3()。並且把將方法的內容改成建立新物件的方式。
1 |
|
執行結果:
1 |
|
輸出結果再度變回跟 function_1 的結果一樣,無法改變外部物件,這是為什麼呢?
Step 1: 跟 function_2 完全相同。初始化字典物件內容。
Step 2: 這時就可以看得出來到底發生了什麼事情了,如果是直接使用建立一個新物件的來賦予變數 obj,那麼將不會改變原本變數 dict_obj 的值。
我們可以得出一個結論,在使用可變物件(Muttable Object)的時候,如果重新指派新的物件給它,它就不會保持共用記憶體,而是建立一個心的物件。
Python 到底是 Pass by Value, Pass Value 還是 Pass Sharing 呢?
都不是,Python 是 Pass by Assignment!
我覺得最清楚的是這篇 Stack Overflow 的回答,簡單解釋,引數傳的是物件的參照(Call by Object Reference),但此 Reference 是由 Pass by Value 的方式實作。
可以歸納出兩個結論:
- 如果你傳遞的可變物件 (Mutable Object),傳遞的是物件的參照(記憶體位址),所以可變物件的值是可以改變的,你可以因此修改到原始的物件的內容。
- 可變物件 (Mutable Object) 有一個例外是你重新指派一個新的物件給他,外部的作用域(Scope)將無法得知你做的事情,所以外部作用域會認舊的物件,而不是新指派的物件。
- 如果你用的是不可變物件(Imutable Object),那麼因為每次指派都會是新的物件,但是物件是在函式作用域內部生成的,所以外部作用域無法得知。就變成了兩個物件
https://stackoverflow.com/questions/986006/how-do-i-pass-a-variable-by-reference
最後再回到官方文件的定義,確認一下 Python 真的是 Pass by Assignment,打完收工。