設計和歷史常見問答集

為什麼 Python 使用縮排將陳述式進行分組?

Guido van Rossum 相信使用縮排來分組超級優雅,並且對提高一般 Python 程式的清晰度有許多貢獻。許多人在學習一段時間之後就愛上了這個功能。

因為沒有開始/結束括號,因此剖析器和人類讀者感知到的分組就不存在分歧。偶爾 C 語言的程式設計師會遇到這樣的程式碼片段:

if (x <= y)
        x++;
        y--;
z++;

如果條件為真,只有 x++ 陳述式會被執行,但縮排會讓很多人對他有不同的理解。即使是資深的 C 語言開發者有時也會盯著他許久,思考為何即便 x > y,但 y 還是減少了。

因為沒有開頭與結尾的括號,Python 比起其他語言會更不容易遇到程式碼風格的衝突。在 C 語言中,有多種不同的方法來放置花括號。在習慣讀寫特定風格後,去讀(或是必須去寫)另一種風格會覺得不太舒服。

很多程式碼風格會把 begin/end 獨立放在一行。這會讓程式碼很長且浪費珍貴的螢幕空間,要概覽程式時也變得較為困難。理想上來說,一個函式應該要佔一個螢幕(大概 20 至 30 行)。20 行的 Python 程式碼比起 20 行的 C 程式碼可以做更多事。雖然沒有開頭與結尾的括號並非單一原因(沒有變數宣告及高階的資料型別同樣有關),但縮排式的語法確實給了幫助。

為什麼我會從簡單的數學運算得到奇怪的結果?

請見下一個問題。

為何浮點數運算如此不精確?

使用者時常對這樣的結果感到驚訝:

>>> 1.2 - 1.0
0.19999999999999996

然後認為這是 Python 的 bug,但這並不是。這跟 Python 幾乎沒有關係,而是和底層如何處理浮點數有關係。

CPython 的 float 型別使用了 C 的 double 型別來儲存。一個 float 物件的值會以固定的精度(通常為 53 位元)存為二進制浮點數,Python 使用 C 來運算浮點數,而他的結果會依處理器中的硬體實作方式來決定。這表示就浮點數運算來說,Python 和 C、Java 等很多受歡迎的語言有一樣的行為。

很多數字可以簡單地寫成十進位表示,但卻無法簡單地以二進制浮點數表示。比方說,在以下程式碼執行後:

>>> x = 1.2

x 裡的值是一個(很接近)1.2 的估計值,但並非精確地等於 1.2。以一般的電腦來說,他實際儲存的值是:

1.0011001100110011001100110011001100110011001100110011 (binary)

而這個值正是:

1.1999999999999999555910790149937383830547332763671875 (decimal)

53 位元的精度讓 Python 可以有 15 至 16 小數位的準確度。

要更完全的解釋可以查閱在 Python 教學的浮點運算一章。

為什麼 Python 字串不可變動?

有許多優點。

其一是效能:知道字串不可變動後,我們就可以在創造他的時候就分配好空間,而後他的儲存空間需求就是固定不變的。這也是元組 (tuple) 和串列 (list) 相異的其中一個原因。

另一個優點是在 Python 中,字串和數字一樣「基本」。沒有任何行為會把 8 這個數值改成其他數值;同理,在 Python 中也沒有任何行為會修改字串「eight」。

為何「self」在方法 (method) 定義和呼叫時一定要明確使用?

此構想從 Modula-3 而來。因為許多原因,他可以說是非常實用。

第一,這樣可以更明顯表現出你在用方法 (method) 或是實例 (instance) 的屬性,而非一個區域變數。即使不知道類別 (class) 的定義,當看到 self.xself.meth(),就會很清楚地知道是正在使用實例的變數或是方法。在 C++ 裡,你可以藉由沒有區域變數宣告來判斷這件事 ── 但在 Python 裡沒有區域變數宣告,所以你必須去看類別的定義來確定。有些 C++ 和 Java 的程式碼規格要求要在實例屬性的名稱加上前綴 m_,所以這種明確性在那些語言也是很好用的。

第二,當你想明確地使用或呼叫在某個類別裡的方法的時候,你不需要特殊的語法。在 C++ 裡,如果你想用一個在繼承類別時被覆寫的基底類別方法,必須要用 :: 運算子 -- 但在 Python 裡,你可以直接寫成 baseclass.methodname(self, <argument list>)。這在 __init__() 方法很好用,特別是在一個繼承的類別要擴充基底類別的方法而要呼叫他時。

最後,他解決了關於實例變數指派的語法問題:因為區域變數在 Python 是(定義為)在函式內被指派值的變數(且沒有被明確宣告成全域),所以會需要一個方法來告訴直譯器這個指派運算是針對實例變數,而非針對區域變數,這在語法層面處理較好(為了效率)。C++ 用宣告解決了這件事,但 Python 沒有,而為了這個原因而引入變數宣告機制又略嫌浪費。但使用明確的 self.var 就可以把這個問題圓滿解決。同理,在用實例變數的時候必須寫成 self.var 即代表對於在方法中不特定的名稱不需要去看實例的內容。換句話說,區域變數和實例變數存在於兩個不同的命名空間 (namespace),而你需要告訴 Python 要使用哪一個。

為何我不能在運算式 (expression) 中使用指派運算?

從 Python 3.8 開始,你可以這麼做了!

指派運算式使用海象運算子 := 來在運算式中指派變數值:

while chunk := fp.read(200):
   print(chunk)

更多資訊請見 PEP 572

為何 Python 對於一些功能實作使用方法(像是 list.index()),另一些使用函式(像是 len(list))?

如 Guido 所說:

(一) 對一些運算來說,前綴寫法看起來會比後綴寫法好 ── 前綴(和中綴!)運算在數學上有更久遠的傳統,這些符號在視覺上幫助數學家們更容易思考問題。想想把 x*(a+b) 這種式子展開成 x*a + x*b 的簡單,再比較一下古老的圈圈符號記法的笨拙就知道了。

(二) 當我看到一段程式碼寫著 len(x),我知道他要找某個東西的長度。這告訴了我兩件事:結果是一個整數、參數是某種容器。相對地,當我看到 x.len(),我必須先知道 x 是某種容器,並實作了一個介面或是繼承了一個有標準 len() 的類別。遇到一個沒有實作對映 (mapping) 的類別卻有 get() 或 keys() 方法,或是不是檔案但卻有 write() 方法時,我們偶爾會覺得困惑。

https://mail.python.org/pipermail/python-3000/2006-November/004643.html

為何 join() 是字串方法而非串列 (list) 或元組 (tuple) 方法?

自 Python 1.6 之後,字串變得很像其他標準的型別,也在此時,一些可以和字串模組的函式有相同功能的方法也被加入。大多數的新方法都被廣泛接受,但有一個方法似乎讓一些程式人員不舒服:

", ".join(['1', '2', '4', '8', '16'])

結果是:

"1, 2, 4, 8, 16"

通常有兩個反對這個用法的論點。

第一項這麼說:「用字串文本 (string literal) (字串常數)看起來真的很醜」,也許真的如此,但字串文本就只是一個固定值。如果方法可以用在值為字串的變數上,那沒道理字串文本不能被使用。

第二個反對意見通常是:「我是在叫一個序列把它的成員用一個字串常數連接起來」。但很遺憾地,你並不是在這樣做。因為某種原因,把 split() 當成字串方法比較簡單,因為這樣我們可以輕易地看到:

"1, 2, 4, 8, 16".split(", ")

這是在叫一個字串文本回傳由指定的分隔符號(或是預設為空白)分出的子字串的指令。

join() 是一個字串方法,因為在用他的時候,你是告訴分隔字串去走遍整個字串序列,並將自己插入到相鄰的兩項之間。這個方法的參數可以是任何符合序列規則的物件,包括自己定義的新類別。在 bytes 和 bytearray 物件也有類似的方法可用。

例外處理有多快?

如果沒有例外被丟出,一個 try/except 區塊是非常有效率的。事實上,抓捕例外要付出昂貴的代價。在 Python 2.0 以前,這樣使用是相當常見的:

try:
    value = mydict[key]
except KeyError:
    mydict[key] = getvalue(key)
    value = mydict[key]

這只有在你預料這個字典大多數時候都有鍵的時候才合理。如果並非如此,你應該寫成:

if key in mydict:
    value = mydict[key]
else:
    value = mydict[key] = getvalue(key)

單就這個情況來說,你也可以用 value = dict.setdefault(key, getvalue(key)),不過只有在 getvalue() 代價不大的時候才能用,畢竟他每次都會被執行。

為什麼 Python 內沒有 switch 或 case 陳述式?

In general, structured switch statements execute one block of code when an expression has a particular value or set of values. Since Python 3.10 one can easily match literal values, or constants within a namespace, with a match ... case statement. An older alternative is a sequence of if... elif... elif... else.

如果可能性很多,你可以用字典去對映要呼叫的函式。舉例來說:

if key in mydict:
    value = mydict[key]
else:
    value = mydict[key] = getvalue(key)

對於呼叫物件裡的方法,你可以利用內建用來找尋特定方法的函式 getattr() 來做進一步的簡化:

class MyVisitor:
    def visit_a(self):
        ...

    def dispatch(self, value):
        method_name = 'visit_' + str(value)
        method = getattr(self, method_name)
        method()

我們建議在方法名稱加上前綴,以這個例子來說是 像是 visit_。沒有前綴的話,一旦收到從不信任來源的值,攻擊者便可以隨意呼叫在你的專案內的方法。

Imitating switch with fallthrough, as with C's switch-case-default, is possible, much harder, and less needed.

為何不能在直譯器上模擬執行緒,而要使用作業系統的特定實作方式?

答案一:很不幸地,直譯器對每個 Python 的堆疊框 (stack frame) 會推至少一個 C 的堆疊框。同時,擴充套件可以隨時呼叫 Python,因此完整的實作必須要支援 C 的執行緒。

答案二:幸運地,無堆疊 (Stackless) Python 完全重新設計了直譯器迴圈,並避免了 C 堆疊。

為何 lambda 運算式不能包含陳述式?

Python 的 lambda 運算式不能包含陳述式是因為 Python 的語法框架無法處理包在運算式中的陳述式。然而,在 Python 裡這並不是一個嚴重的問題。不像在其他語言中有獨立功能的 lambda,Python 的 lambda 只是一個在你懶得定義函式時可用的一個簡寫表達法。

函式已經是 Python 裡的一級物件 (first class objects),而且可以在區域範圍內被宣告。因此唯一用 lambda 而非區域性的函式的優點就是你不需要多想一個函式名稱 — 但這樣就會是一個區域變數被指定成函式物件(和 lambda 運算式的結果同類)!