剛接觸到 Python 時,很可能會遇到類似靈異現象: 1
2
3
4
5
6
7
8
9
10 a = []
b = a
1) b.append(
a
[1]
1 a =
b = a
1 b +=
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 | 'starbuststream' a = |
對一個變數,你可以做的事情包含:
1. 引用它
讀取和修改的行為,都算是引用。
1 | a = 2 |
2. 指向至一個物件上(賦值)
=
運算子做的就是賦值。=
將一個名稱指向一個物件。如果指定的命名空間不存在這個名稱,那 Python 會創建一個新的。(命名空間為名稱的集合)
1 | owo = [] # 將 owo 賦值 |
如果用變數將另一個變數賦值,則兩個變數會指向同一個物件:
1 | 'starbuststream' a = |
3. 將其從命名空間刪除
我們可以在指定的命名空間新增名稱,反之我們也可以透過 del
關鍵字把它刪除。
1 | 'starbuststream' a = |
要注意的是:變數僅僅是一個名稱。我們將這個名稱刪除,實際上不會影響到記憶體裡的物件。對於字串物件 'starbuststream'
來說,僅僅是少了一個參考至它自己的變數而已。
1 | 'starbuststream' b = a = |
題外話: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 | 1 a = |
如果理解上個小節所說的,應該可以想到: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 | 0 a = |
執行 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 | 1 a = |
1 | def outer(): |
Python 的參數傳遞
Python 的參數傳遞,相當於做了一次賦值。將變數作為容器的語言中,有 call by reference 或 call by value 的概念,在 Python 中則沒有。
1 | 1 a = |
為什麼需要有 Immutable Objects?
Immutable Objects 有以下優點,但我現在還沒體會必要性:
保證一但被創建出來 hash 值就不會改變,才能做為 hash table 的 key
唯讀保證了多線程安全
Python 的機制使多個變數會指向同一個物件,不變性保證了每個變數參考到的物件始終相同