總有一天會用到的筆記

本站為減緩筆者下列疑難雜症誕生:記性很差,學過就忘;智商低落,囫圇吞棗。

0%

【Python】關於 Immutable 和 Mutable,以及參數傳遞

剛接觸到 Python 時,很可能會遇到類似靈異現象:

1
2
3
4
5
6
7
8
9
10
>>> a = []
>>> 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 裡變數沒有型別。你可以隨時隨地將一個名稱指向至另一個物件。

為了方便示例,這裡介紹 idisid(foo) 會將傳入變數所指向的物件的 id (可當作記憶體位址)印出來。而 a is b 會比較兩者所指向的物件,是否為同一實體,也就是 id(a) == id(b)

1
2
3
4
5
>>> a = 'starbuststream'
>>> id(a)
4351113008
>>> a is a
True

對一個變數,你可以做的事情包含:

1. 引用它

讀取和修改的行為,都算是引用。

1
2
3
4
5
6
a = 2
b = 3
print(a+b) # 引用 a 和 b

owo = []
owo.append(1) # 引用 owo

2. 指向至一個物件上(賦值)

= 運算子做的就是賦值。= 將一個名稱指向一個物件。如果指定的命名空間不存在這個名稱,那 Python 會創建一個新的。(命名空間為名稱的集合)

1
2
3
4
5
6
owo = []    # 將 owo 賦值
owo.append(1)

a = 2 # 將 a 賦值
a = a+2 # 將 a 賦值,右邊 a+2 引用 a
print(a)

如果用變數將另一個變數賦值,則兩個變數會指向同一個物件:

1
2
3
4
>>> a = 'starbuststream'
>>> b = a
>>> a is b
True

3. 將其從命名空間刪除

我們可以在指定的命名空間新增名稱,反之我們也可以透過 del 關鍵字把它刪除。

1
2
3
4
5
6
>>> a = 'starbuststream'
>>> del a
>>> print(a)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'a' is not defined

要注意的是:變數僅僅是一個名稱。我們將這個名稱刪除,實際上不會影響到記憶體裡的物件。對於字串物件 'starbuststream' 來說,僅僅是少了一個參考至它自己的變數而已。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> b = a = 'starbuststream'
>>> id(a)
4351113008
>>> id(b)
4351113008
>>> del a
>>> a
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'a' is not defined
>>> b
'starbuststream'
>>> id(b)
4351113008

題外話: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
2
3
4
5
6
>>> a = 1
>>> id(a)
4348150000
>>> a += 1
>>> id(a)
4348150032

如果理解上個小節所說的,應該可以想到:a += 1 實際上做的事,是 a = a+1。其中 a+1 是對 a 的引用,並且生成了一個新物件 int(2)= 則是把 a 重新指向到右側生成的新物件。

對於引言例子的原理就豁然開朗了。當我們使用 my_lis?t.append() 時,我們僅是引用;而使用 my_int += 1 時,我們其實進行了隱性的賦值。

1
2
3
4
5
6
7
8
9
10
>>> a = []
>>> b = a
>>> b.append(1)
>>> a
[1]
>>> a = 1
>>> b = a
>>> b = 2
>>> a
1

若我們將兩者都做賦值,mutable 跟 immutable 的表現是完全一樣的:

1
2
3
4
5
6
7
8
9
10
>>> a = []
>>> b = a
>>> b = ['owo'] # 將 b 指定至另一個物件
>>> a is b
False
>>> a = 1
>>> b = a
>>> b = 2 # 將 b 指定至另一個物件
>>> a is b
False

作用域和 Mutablility 的關係

1
a = 1

引用某個變數時,Python 會先查找當前命名空間 local 裡是否有這個名稱,如果沒有則一層一層往外找,直到 global,接著是 builtin。賦值時,若 local 有這個名稱沒事;若沒有,Python 也不會往外找,而是直接新增一個名稱在 local換句話說,外層的名稱是唯讀的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> a = 0
>>> def show():
... print(a)
...
>>> show()
0
>>> def add():
... a += 1
...
>>> add()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in add
UnboundLocalError: local variable 'a' referenced before assignment

執行 a += 1,也就是 a = a+1 時,a 首先被新增到 local 命名空間,接著 a+1 引用剛被新增的名稱 a。此時會發現 a 尚未被指定到任何一個物件!反之,若改為 list 並在函式內執行 append,因為是引用,不會在 local 裡新增名稱,也就可以直接修改到外面的 list 了。

global 和 nonlocal 關鍵字。

解法很簡單。global 語句宣告了該名稱位於全域,Python 不需要在苦苦搜尋該名稱。

1
2
3
4
5
6
7
>>> def add():
... global a
... a = 1
...
>>> add()
>>> a
1

nonlocal 語句宣告該名稱不在 localglobal 裡。由於作用域覆蓋當前的 nonlocal 命名空間可能有很多個,使用 nonlocal 需要明確的指出該名稱位於哪一個命名空間,只能對預先存在的名稱做 nonlocal 宣告。

1
2
3
4
5
6
7
8
9
10
11
>>> a = 1
>>> def outer():
... a = 2
... def inner():
... nonlocal a
... a += 1
... inner()
... print(a)
...
>>> outer()
3
1
2
3
4
5
6
>>> def outer():
... def inner():
... nonlocal qwq # 只能對預先存在的名稱做 nonlocal 宣告
...
File "<stdin>", line 3
SyntaxError: no binding for nonlocal 'qwq' found

Python 的參數傳遞

Python 的參數傳遞,相當於做了一次賦值。將變數作為容器的語言中,有 call by reference 或 call by value 的概念,在 Python 中則沒有。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> a = 1
>>> def add(x):
... x += 1 # 賦值
...
>>> add(a) # 相當於 x = a
>>> a
1
>>> b = []
>>> def ap(x):
... x.append(1) # 引用
...
>>> ap(b) # 相當於 x = b
>>> b
[1]

為什麼需要有 Immutable Objects?

Immutable Objects 有以下優點,但我現在還沒體會必要性:

  • 保證一但被創建出來 hash 值就不會改變,才能做為 hash table 的 key

  • 唯讀保證了多線程安全

  • Python 的機制使多個變數會指向同一個物件,不變性保證了每個變數參考到的物件始終相同