簽章 <<
Previous Next >> 物件導向
複製與參照
這邊整理關於 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 的指派
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;
總結:除非特別設計,不然指派運算子都是使用複製,而非參照。例如 C++ 中的「函式成本」是由於下列文法:
// 函式是將程式碼封裝後嵌入於主程式中。
int plus(int a, int b) { return a + b; }
// 使用 plus
int a = 10;
int b = 20;
int c = plus(a, b);
等價於:
// 轉換後:
int a = 10;
int b = 20;
int c;
{
int _a = a;
int _b = b;
int _r = _a + _b;
c = _r;
}
這樣每個參數都會造成龐大的傳輸成本,假如參數是攜帶龐大資料的容器,就更不得了了。因此使用 inline 函式、參照參數,或指標傳入、傳出,以節省執行時間、記憶空間。
上述流程是不是十分繁瑣且危險?在 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。不過只有部分字面數值,任何時候寫出來都永遠共享記憶體,特徵是不能改變值 (Mutable),所有 method 操作結果都回傳副本,不改原始值。只有 None、bool、int、float、complex、所有字串 (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
if __name__ == '__main__':
main()
簽章 <<
Previous Next >> 物件導向