剛接觸到 Python 時,很可能會遇到類似靈異現象: 1
2
3
4
5
6
7
8
9
10a = []
b = a
b.append(1)
a
[1]
a = 1
b = a
b += 1
a
1
又或者,對於 Python 時而 pass by reference,時而 pass by value 有所疑惑。如果嘗試搜尋,可能會發現它們皆與今天的主題有關。
Python 的變數是什麼?
首先來談最基本的問題:Python 的變數是什麼?這個問題並非你想像的理所當然。
在我們熟悉的語言如 C++ 裡,宣告變數,等同配置了一段記憶體,用以儲存資訊。在引用該段變數時,不管是讀取、修改、賦值,相當於在操作這段記憶體內的資料。也就是說在 C++ 裡,變數是某段記憶體的別名;這也說明了為什麼 C++ 需要你給出型別才能宣告,否則電腦根本不知道該配置多少記憶體給這個變數。
在 Python 裡,任何東西都是物件,而變數僅是從「名稱」到「物件」的映射。在 Python 裡對一個變數賦值,代表了你將這個名稱「指向」到某個物件上。因此,在 Python 裡變數沒有型別。你可以隨時隨地將一個名稱指向至另一個物件。
為了方便示例,這裡介紹 id 和 is。id(foo) 會將傳入變數所指向的物件的 id (可當作記憶體位址)印出來。而 a is b 會比較兩者所指向的物件,是否為同一實體,也就是 id(a) == id(b)。
1 | a = 'starbuststream' |
對一個變數,你可以做的事情包含:
1. 引用它
讀取和修改的行為,都算是引用。
1 | a = 2 |
2. 指向至一個物件上(賦值)
= 運算子做的就是賦值。= 將一個名稱指向一個物件。如果指定的命名空間不存在這個名稱,那 Python 會創建一個新的。(命名空間為名稱的集合)
1 | owo = [] # 將 owo 賦值 |
如果用變數將另一個變數賦值,則兩個變數會指向同一個物件:
1 | a = 'starbuststream' |
3. 將其從命名空間刪除
我們可以在指定的命名空間新增名稱,反之我們也可以透過 del 關鍵字把它刪除。
1 | a = 'starbuststream' |
要注意的是:變數僅僅是一個名稱。我們將這個名稱刪除,實際上不會影響到記憶體裡的物件。對於字串物件 'starbuststream' 來說,僅僅是少了一個參考至它自己的變數而已。
1 | b = a = 'starbuststream' |
題外話:Python 的回收機制中,當參考至某物件的數量等於零,也就是再也不可能透過任何方式引用它,它就有可能被刪除。
關於物件的值:Immutable vs Mutable
根據官方文件,每個物件都由 id, type, 和 value 組成。有些物件的值是可變的,有些則否。這引出了今天的主題:Immutable vs Mutable。
immutable 不像 mutable 物件,提供了可以修改成員屬性的方法。這代表 immutable 是唯讀的。immutable object 有:int, str, float, bool, tuple, frozenset 等。而常見的 mutable object 有 list, dict, 以及自訂 class 等。
這不符合我們的經驗,int 怎麼可能不可修改?
1 | a = 1 |
如果理解上個小節所說的,應該可以想到:a += 1 實際上做的事,是 a = a+1。其中 a+1 是對 a 的引用,並且生成了一個新物件 int(2);= 則是把 a 重新指向到右側生成的新物件。
對於引言例子的原理就豁然開朗了。當我們使用 my_lis?t.append() 時,我們僅是引用;而使用 my_int += 1 時,我們其實進行了隱性的賦值。
1 | a = [] |
若我們將兩者都做賦值,mutable 跟 immutable 的表現是完全一樣的:
1 | a = [] |
作用域和 Mutablility 的關係
1 | a = 1 |
引用某個變數時,Python 會先查找當前命名空間 local 裡是否有這個名稱,如果沒有則一層一層往外找,直到 global,接著是 builtin。賦值時,若 local 有這個名稱沒事;若沒有,Python 也不會往外找,而是直接新增一個名稱在 local。換句話說,外層的名稱是唯讀的。
1 | a = 0 |
執行 a += 1,也就是 a = a+1 時,a 首先被新增到 local 命名空間,接著 a+1 引用剛被新增的名稱 a。此時會發現 a 尚未被指定到任何一個物件!反之,若改為 list 並在函式內執行 append,因為是引用,不會在 local 裡新增名稱,也就可以直接修改到外面的 list 了。
global 和 nonlocal 關鍵字。
解法很簡單。global 語句宣告了該名稱位於全域,Python 不需要在苦苦搜尋該名稱。
1 | def add(): |
nonlocal 語句宣告該名稱不在 local 和 global 裡。由於作用域覆蓋當前的 nonlocal 命名空間可能有很多個,使用 nonlocal 需要明確的指出該名稱位於哪一個命名空間,只能對預先存在的名稱做 nonlocal 宣告。
1 | a = 1 |
1 | def outer(): |
Python 的參數傳遞
Python 的參數傳遞,相當於做了一次賦值。將變數作為容器的語言中,有 call by reference 或 call by value 的概念,在 Python 中則沒有。
1 | a = 1 |
為什麼需要有 Immutable Objects?
Immutable Objects 有以下優點,但我現在還沒體會必要性:
保證一但被創建出來 hash 值就不會改變,才能做為 hash table 的 key
唯讀保證了多線程安全
Python 的機制使多個變數會指向同一個物件,不變性保證了每個變數參考到的物件始終相同