什麼是 python Pass by Assignment?
這幾天在刷題寫 leetcode,用最方便的 python 語言來刷,在寫到 39. Combination Sum 這題的時候,為了要滿足遞迴的條件,所以在遞迴函式中加入當前答案 res=list() 當做參數,結果發現…這個 List 面試的數值怎麼一直變來變去啦…明明我沒有去動到它的阿…
keywords: Pass by Assignment
以下是犯人 code:
1 | from typing import List |
發生事情當下我心想:該不會是發生了 call by reference 吧…,所以特別花了一點時間來研究一下,python 中傳值的方式倒底是怎麼傳
Call by Value vs Call by Reference
在開始講 python 是怎麼傳值前,我們要先來了解在 python 中是如何定義資料型態的,以下的例子我會舉 JS 當成是另一個語言來做比較
在 JS (以及一部份語言) 中所有的資料型態分為兩種:primitive type 以及 object type
所謂的 primitive type (中文翻原始型別),在 JS 中有以下類別:
- String
- Number (int, float, ...)
- Boolean
- Null
- Undefined
而大部份你看得到的「其它」類別,都是 object type,也就是都是由一個 object 定義出來的
- Object
- Function
- Array
- Set
而在 JS 中:primitive type 全部都是 call by value,也就是傳進函式前,complier 會先幫你在記憶體中新增一塊不同位置,但是值相同
1 | function call_by_value(x) { |
其它的 Object type 則是 call by reference,也就是不管是在函式內外,變數指向的記憶體位置都是一樣的,所以在 call_by_reference() 函式內的改動,會影響到函式外的變數:
1 | let person = { |
Mutable vs Immutable
而在 python 中比較不一樣的是,python 中所有的型別都是一個物件 (object),每一個資料都會有一個 __class__ 屬性來看看是由哪一個 class 所生成的
1 | x = 100 |
同時也可以使用 id 來看看這個變數是放在記憶中哪一個位置
1 | print(id(x)) # 4342041840 |
因為每一個型別都是一個物件,所以不會像其它語言的分法一樣,在 python 中是透過是不是 mutable 來區分的。根據網路上別人留言的定義 mutable 的意思是
1 | // a mutable object is an object that can be changed |
下表是 python 中常見型別是不是 mutable/immutable 的表格。圖片參考自 Mutable vs Immutable Objects in Python

接下來詳細介紹這兩個的差別:
immutable 物件一旦被創造出來,它的值就永遠不可以再更改,像是 int、float、string、bool、tuple。用下面簡單的例子來舉例:
1 | x = 100 |
可以發現因為 x 是 int 型別,是屬於 immutable,一旦值發生改變 python 是直接會再找一塊新的記憶體來存 x 變數,而非修改原本記憶體的值
不知道大家看到這邊的時候有沒有覺得很奇怪,為什麼 python 要這麼沒有效率的一直新增記憶體空間阿?我們不訪試著想想看,如果今天是在 C 中我們重複定義了一個變數會發生什麼事:
1 | int x = 100; |
它會噴重複定義的錯,因為 x 所在的記憶體位置已經被使用了,當我們想再定義一次時,complier 會提醒我們不能這麼做。但是有沒有想過,為什麼 python 可以這麼做呢?python 雖然少了型別 (int) 的部份,但還是可以達成下面程式。
1 | x = 100 |
其實我們之所以可以在 python 裡面執行這種操作,就是因為每當使用 = 去 assign 一個變數時,python 都會在記憶體中新增一塊位置存放它,也就是說其實這兩個 x 根本是不一樣的東西,而這也是 immutable 的精神所在:值絕對不會被更改
如果用圖片的方式來表達的話,python 中的執行方式,就會如下圖所式:

到這邊就引出這篇文章最重要的想法:這種每當有 assignment 發生時,immutable 型別所指向的記憶體都會改變,這種方法在 python official document 中稱作:Pass by Assignment。
我們現在來看看如果把 Pass by Assignment 的想法加上 function 會發生什麼事,以下範例我們將 immutable 變數當成參數傳進 function:
1 | def fun(x, y): |
當 a b immutable 變數傳進 function 時 python 會像其它的語言一樣,新增一塊記憶體並且 copy 變數的值到這個記憶體中,不管在 function 中的任何操作都不會影響到外面 a, b 變數。
也就是說 python 的 immutable 型別在 Pass by Assignment 中,很像在其它語中稱作的 Call by Value,記憶體操作如下圖表示:

接下來是換 mutable 的部份。mutable 型別的有:list、set、dict,這些型別如同 mutable 的意義一樣:是可以在宣告後修改的,也就是在同一個記憶體位置中修改存的值,我們用以下的範例來看看:
1 | my_list = [1, 3, 6] |

可以發現不管我們對 my_list 做:修改值、append 等操作,id(my_list) 都是不會變的,而正是 mutable 的主要表現:可以在相同記憶體下修改其中的值
那如果 mutable 型別遇上 function 會發生什麼事呢?看看下面程式的例子:
1 | def fun(x, y): |
可以發現,當 a b 傳到 function 後,當 function 內部的 x y 修改值後,外部的 a b 同時也會一起修改
當 a b mutable 變數傳進 function 時,python 會將 function 內的變數 x 指向 a 所指的記憶體位置,使得在 function 內修改值時,function 外也會同時被修改 (因為就是同一個東西)
也就是說 python 的 mutable 型別在 Pass by Assignment 中,很像在其它語中稱作的 Call by Reference,記憶體操作如下圖表示:

但是與正常 Call by Reference 不一樣的是,如果我們在 function 內是用 assignment 重新給定一個 mutable 變數值時,python 會像 Call by Value 一樣重新找一塊新的記憶體放,而 function 內外的值互不相影響,如下圖:
1 | def fun(x): |

可以發現當 x = [0] 後 python 竟然是重新找一個記憶體去存放,所以當然也不會動到 a 裡面的值,而正是 python Call by Assignment 最要留意的一個點,它並非「完全的」Call by Reference 喔 ~
Reference
JS基礎:Primitive type v.s Object types
(圖片主要參考來源) [Python 基礎教學] 什麼是 Immutable & Mutable objects