Python 複製與參照

這邊整理關於 Python 程式語言中的「命名」規則,請學員閱讀。

Python 的設計構想源自 ABC[1],採越位規格 (off-side rule) 分隔程式區段,其表達式語法 (expression syntax) 與 C 語言類似[2],因此會提到其相關規則。在其他類似 C 的程式語言(C++、C#、Java 等,其中 C++ 文法的相似度最高)可能會使用相同的規則處理。

請牢記以下關係:

  • 官方提供的 Python 單機版直譯器稱為 CPython,因為該直譯器是純 C 語言編寫,達成 Python 所有功能。除了 CPython 外,還有以 Java 編寫的 Jython,以 .net 技術編寫的 Ironpython,以 Python 編寫的 PyPy, 以 Javascript 編寫的 Brython,進階版的 Stackless Python 以及專用於微控制器的 Micropython 等。CPython 的原理是將 Python script 翻譯成字節碼 (Bytecode),並透過解析字節碼來執行,字節碼存放在 __pycache__ 資料夾中。

  • 由於 Python 文法是由擴展巴克斯範式 (Extended Backus–Naur form, EBNF) 定義的(詳見此),因此理論上任何支援 EBNF 解析功能的程式語言都能寫成 Python 直譯器,包括 Python 本身 (PyPy)。

以下章節內容關於非 Python 部分可以斟酌吸收。

C 的指派

在 C 語言中,使用指派運算子來規劃記憶體存值,該符號為等於記號 =

int a = 10;

上面 C 程式碼的意思為:使用最大為整數 (int) 的記憶體空間存放數字 10,並且在本程式範圍 (Scope) 中,這個記憶體空間的代號為 a

在 C 語言中記憶體空間的代號稱為變數 (Variable),透過「宣告」規定在範圍中有哪些代號;「定義」可以做記憶體劃分和存值的動作。

/*
C 語言的單行註解使用雙斜線,多行使用斜線與星號。
每「行」結尾為分號,會忽略重複空白和所有換行記號。
因此,C 語言可以不用縮排、全部寫同一行,難以閱讀。
*/

// C 語言可以任意規劃「範圍」或巢狀範圍,使用大括弧即可。
{
    // 宣告,沒有任何動作,只是給編譯器看的。
    int a;
    // 定義,規劃記憶體並存值。
    a = 10;
    {
        // 可以一起寫。
        int b = 20;
        // 如果巢狀範圍宣告撞名,會將外部範圍的名稱暫時「隱藏」。
    }
    // 這裡不能用 b。
}

在宣告的範圍外,該變數會被自動刪除,以節省記憶體。因此,在 C 語言中,任何定義的動作都會花費記憶空間。「指派」的文法如下:

左值 = 右值

右值的計算結果為任意記憶體大小的值,而左值必須運算出相應大小的記憶體空間,透過指派運算子,可以將右值的值複製到左值。另外,一些運算類的指派運算子 +=-= 等,是將計算結果存入相同記憶體位置的意思,如 a += 10 同於 a = a + 10

甚至在範圍外,也可以使用 C 語言的指標 (Pointer) 功能,攜帶記憶體空間的鑰匙,持有鑰匙,可以直接存取記憶體。以下為 C++ 語言的一個小範例。

/*
指標是一種變數!

指標是一種二進位代號,代表記憶體區段的「第一個值」。
指標的間隔數是該類型的空間大小,但是 C 語言中 +1 會自動幫忙跳號。
*/

// 假設有一把鑰匙(還沒定義)。
int *a;
{
    // 使用 new 關鍵字初始化 50 個整數,並將第一把鑰匙交給 b。
    int *b = new int[50];
    // 將 b 鑰匙複製給 a。
    a = b;
}
/*
b 鑰匙被刪除,但是 50 個整數還在。
如果剛才沒有複製給 a,會遺失 50 個整數的鑰匙,引發記憶體洩漏 (Memory leak)。
*/
cout << *(a + 2) << endl;  // 顯示第三位整數的值(第一把鑰匙 +2)。
cout << a[0] << endl;  // 同上,顯示第一位整數的值。
delete[] a;  // 使用 delete 關鍵字刪除 a 鑰匙的所有值。

若不使用指標,C 語言也有參照物件,相當於取綽號:

int a = 10;
// 為 a 變數取綽號 b。
int &b = a;
b = 2;  // a = 2

總結:除非特別設計,不然指派運算子都是使用複製,而非參照。

上述流程是不是十分繁瑣且危險?在 Python 中,參考自其他程式語言,引入垃圾回收機制 (Garbage collection),取代了指標和參照物件的功能。

Python 的指派

Python 的變數稱為名稱 (Name),可以視作一張範圍通行證,而非記憶體代號,已經跟 C 語言的意思不一樣了。而透過「指派」,可以發通行證給任何數值,不用管記憶體大小。一句話解釋規則

  • 所有的數值會自動追蹤與管理,擁有一個或多個名稱的值可以在該作用範圍使用,失去所有名稱的值會被刪除

Python 的表達式 (Expression) 中有種數值稱為字面數值 (literal value),意指寫出來就是該值,例如 70 的類型是 int(1, 2, 3) 的類型是 tuple[1, 2, 3] 的類型是 list{'a': 20, 'c': 80} 的類型是 dict。不過只有部分字面數值,任何時候寫出來都永遠共享記憶體,特徵是不能改變值,所有 method 操作結果都回傳副本,不改原始值。只有 Noneboolintfloatcomplex、所有字串 (string)、tuple 類型。

# 相同值的共享記憶體檢查
# 用 is 運算子可以檢查是否為相同記憶體。
# int
print(10 is 10)  # True
# tuple
print(() is ())  # True
# list
print([] is [])  # False
# dict
print({} is {})  # False

來段範例:

# 發通行證 a 給數值 10。
a = 10
# 通行證 a 的持有者是 10,發通行證 b 給數值 10。
b = a
print(a is b)  # True

# b += 5 來自 C 語言,同於 b = b + 5。
# 對通行證 b 的值做 +5 計算,並對該值發通行證 b。
# 通行證 b 被拔除自 10,交給結果 15。
b += 5
print(b)  # 15
print(a)  # 10
print(a is b)  # False

來一段容器的範例:

# list 容器
a = [1, 2, 3]
b = a
print(a is b)  # True

# 對通行證 b 的容器操作,在尾端加入 4。
b.append(4)
# 使用 del 關鍵字,對通行證 b 的容器操作,刪除第二位值 2。
del b[1]
print(b)  # [1, 3, 4]
print(a)  # [1, 3, 4]

# 幫當前的 a 值複製,重發通行證 b 給複製體。
b = a.copy()
# 讓 a 刪除最後一項。
a.pop()
print(a)  # [1, 3]
print(b)  # [1, 3, 4]
print(a is b)  # False

活用上述概念,可以輕鬆達成複製 (Copy) 與參照 (Reference) 功能。

最後介紹範圍的規則:

# Python 中,唯二的範圍界線為 Function 與 Class 的定義,而非縮排。

def func(b):
    # 通行證 c 只有效於 func 中。
    # 回傳值計算完後通行證 c 會被移除(垃圾回收)。
    c = 20
    return b + c

# 全域通行證 g
g = 'abc'

# 主程式與 func 的範圍沒有巢狀關係。
def main():
    a = 10
    # a 進入 func 函數,獲得區域通行證 b。
    # 回傳值 30 被丟出 func,獲得區域通行證 d。
    d = func(a)
    print(d)  # 30

    # 巢狀函式有巢狀範圍界線。
    def nest_func():
        # 使用 global 關鍵字指定全域通行證 g。
        global g
        # 全域通行證 g 重新發給 50。
        g = 50
        # 使用 nonlocal 關鍵字指定上層的區域通行證 d。
        nonlocal d
        # 上層的區域通行證 d 重新發給 20。
        d = 20

    # 執行巢狀函式。持有 d 的 30 被刪除;持有 g 的 'abc' 被刪除。
    nest_func()
    print(g)  # 50
    print(d)  # 20

# 執行 main
main()
Show Comments