設計和歷史常見問答集

目錄

為什麼 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 陳述式?

你可以用一連串的 if... elif... elif... else 來輕易達成相同的效果。對於單純的值或是在命名空間內的常數,你也可以使用 match ... case 陳述式。

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

functions = {'a': function_1,
             'b': function_2,
             'c': self.method_1}

func = functions[value]
func()

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

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

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

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

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

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

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

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

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

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

Python 可以被編譯成機器語言、C 語言或其他種語言嗎?

Cython 可以編譯一個調整過有選擇性註解的 Python 版本。 Nuitka 是一個有潛力編譯器,可以把 Python 編譯成 C++,他的目標是支援完整的 Python 語言。

Python 如何管理記憶體?

Python 記憶體管理的細節取決於實作。Python 的標準實作 CPython 使用參照計次 (reference counting) 來偵測不再被存取的物件,並用另一個機制來收集參照循環 (reference cycle)、定期執行循環偵測演算法來找不再使用的循環並刪除相關物件。 gc 模組提供了可以執行垃圾收集、抓取除錯統計數據和調整收集器參數的函式。

然而,在其他實作(像是 JythonPyPy)中,會使用像是成熟的垃圾收集器等不同機制。如果你的 Python 程式碼的表現取決於參照計次的實作,這個相異處會導致一些微小的移植問題。

在一些 Python 實作中,下面這段程式碼(在 CPython 可以正常運作)可能會把檔案描述子 (file descriptor) 用盡:

for file in very_long_list_of_files:
    f = open(file)
    c = f.read(1)

實際上,使用 CPython 的參照計次和解構方案 (destructor scheme),每個對f的新指派都會關閉前面打開的檔案。然而用傳統的垃圾回收 (GC) 的話,這些檔案物件只會在不固定且有可能很長的時間後被收集(並關閉)。

如果你希望你的程式碼在任何 Python 實作版本中都可以運作,那你應該清楚地關閉檔案或是使用 with 陳述式,如此一來,不用管記憶體管理的方法,他也會正常運作:

for file in very_long_list_of_files:
    with open(file) as f:
        c = f.read(1)

為何 CPython 不使用更多傳統的垃圾回收機制?

第一,這並不是 C 的標準功能,因此他的可攜性低。(對,我們知道 Boehm GC 函式庫。他有可相容於大多數平台的組合語言程式碼,但依然不是全部,而即便它大多數是通透的,也並不完全,要讓它跟 Python 相容還是需要做一些修補。)

傳統的垃圾收集 (GC) 在 Python 被嵌入其他應用程式時也成了一個問題。在獨立的 Python 程式裡當然可以把標準的 malloc() 和 free() 換成 GC 函式庫提供的其他版本;但一個嵌著 Python 的應用程式可能想用自己的 malloc() 和 free() 替代品,而不是用 Python 的。以現在來說,CPython 和實作 malloc() 和 free() 的程式相處融洽。

當 CPython 結束時,為何所有的記憶體不會被釋放?

當離開 Python 時,從 Python 模組的全域命名空間來的物件並非總是會被釋放。在有循環引用的時候,這可能會發生。有些記憶體是被 C 函式庫取用的,他們不可能被釋放(例如:像是 Purify 之類的工具會抱怨)。然而,Python 在關閉的時候會積極清理記憶體並嘗試刪除每個物件。

如果你想要強迫 Python 在釋放記憶體時刪除特定的東西,你可以用 atexit 模組來執行會強制刪除的函式。

為何要把元組 (tuple) 和串列 (list) 分成兩個資料型態?

串列和元組在很多方面相當相似,但通常用在完全不同的地方。元組可以想成 Pascal 的紀錄 (record) 或是 C 的結構 (struct),是一小群相關聯但可能是不同型別的資料集合,以一組為單位進行操作。舉例來說,一個笛卡兒坐標系可以適當地表示成一個有二或三個值的元組。

另一方面,串列更像是其他語言的陣列 (array)。他可以有不固定個同類別物件,且為逐項操作。舉例來說,os.listdir('.') 回傳當下目錄裡的檔案,以包含字串的串列表示。如果你新增了幾個檔案到這個目錄,一般來說操作結果的函式也會正常運作。

元組則是不可變的,代表一旦元組被建立,你就不能夠改變裡面的任何一個值。而串列可變,所以你可以改變裡面的元素。只有不可變的元素可以成為字典的鍵,所以只能把元組當成鍵,而串列則不行。

串列 (list) 在 CPython 中是怎麼實作的?

CPython 的串列 (list) 事實上是可變長度的陣列 (array),而不是像 Lisp 語言的鏈接串列 (linked list)。實作上,他是一個連續的物件參照 (reference) 陣列,並把指向此陣列的指標 (pointer) 和陣列長度存在串列的標頭結構內。

因此,用索引來找串列特定項 a[i] 的代價和串列大小或是索引值無關。

當新物件被新增或插入時,陣列會被調整大小。為了改善多次加入物件的效率,我們有用一些巧妙的方法,當陣列必須變大時,會多收集一些額外的空間,接下來幾次新增時就不需要再調整大小了。

字典 (dictionaries) 在 CPython 中是怎麼實作的?

CPython 的字典是用可調整大小的雜湊表 (hash table) 實作的。比起 B 樹 (B-tree),在搜尋(目前為止最常見的操作)方面有更好的表現,實作上也較為簡單。

字典利用內建 hash() 函式,對每個鍵做雜湊計算。雜湊結果依據鍵的值和個別執行緒 (processes) 的種子而有相當大的差距。例如,"Python" 的雜湊是 -539294296,而只差一個字的 "python" 則是 1142331976。雜湊結果接著被用來計算值在內部陣列儲存的位置。假設你存的鍵都有不同的雜湊值,那字典只需要常數時間 — 用大 O 表示法 (Big-O notation) 就是 O(1) — 來找任意一個鍵。

為何字典的鍵一定是不可變的?

實作字典用的雜湊表是根據鍵的值做計算從而找到鍵的。如果鍵可變的話,他的值就可以改變,則雜湊的結果也會一起變動。但改變鍵的物件的人無從得知他被用來當成字典的鍵,所以無法修改字典的內容。然後,當你嘗試在字典中尋找這個物件時,因為雜湊值不同的緣故,你找不到他。而如果你嘗試用舊的值去尋找,也一樣找不到,因為他的雜湊結果和原先物件不同。

如果你想要用串列作為字典的索引,把他轉換成元組即可。tuple(L) 函式會建立一個和串列 L 一樣內容的元組。而元組是不可變的,所以可以用來當成字典的鍵。

也有人提出一些不能接受的方法:

  • 用串列的記憶體位址(物件 id)來雜湊。這不會成功,因為你如果用同樣的值建立一個新的串列,是找不到的。舉例來說:

    mydict = {[1, 2]: '12'}
    print(mydict[[1, 2]])
    

    這將會導致 KeyError 例外,因為 [1, 2] 的 id 在第一行和第二行是不同的。換句話說,字典的鍵應該要用 == 來做比較,而不是用 is

  • 複製一個串列作為鍵。這一樣不會成功,因為串列是可變的,他可以包含自己的參照,所以複製會形成一個無窮迴圈。

  • 允許串列作為鍵,但告訴使用者不要更動他。當你不小心忘記或是更動了這個串列,會產生一種難以追蹤的 bug。他同時也違背了一項字典的重要定則:在 d.keys() 的每個值都可以當成字典的鍵。

  • 一旦串列被當成鍵,把他標記成只能讀取。問題是,這不只要避免最上層的物件改變值,就像用元組包含串列來做為鍵。把一個物件當成鍵,需要將從他開始可以接觸到的所有物件都標記成只能讀取 — 所以再一次,自己參照自己的物件會導致無窮迴圈。

如果你需要的話,這裡有個小技巧可以幫你,但請自己承擔風險:你可以把一個可變物件包裝進一個有 __eq__()__hash__() 方法的類別。只要這種包裝物件還存在於字典(或其他類似結構)中,你就必須確定在字典(或其他用雜湊為基底的結構)中他們的雜湊值會保持恆定。

class ListWrapper:
    def __init__(self, the_list):
        self.the_list = the_list

    def __eq__(self, other):
        return self.the_list == other.the_list

    def __hash__(self):
        l = self.the_list
        result = 98767 - len(l)*555
        for i, el in enumerate(l):
            try:
                result = result + (hash(el) % 9999999) * 1001 + i
            except Exception:
                result = (result % 7777777) + i * 333
        return result

請注意,雜湊的計算可能變得複雜,因為有串列成員不可雜湊 (unhashable) 和算術溢位的可能性。

此外,不管物件是否在字典中,如果 o1 == o2(即 o1.__eq__(o2) is True),則 hash(o1) == hash(o2)(即 o1.__hash__() == o2.__hash__()),這個事實必須要成立。如果無法滿足這項限制,那字典和其他用雜湊為基底的結構會出現不正常的行為。

至於 ListWrapper,只要這個包裝過的物件在字典中,裡面的串列就不能改變以避免不正常的事情發生。除非你已經謹慎思考過你的需求和無法滿足條件的後果,不然請不要這麼做。請自行注意。

為何 list.sort() 不是回傳排序過的串列?

在重視效能的情況下,把串列複製一份有些浪費。因此,list.sort() 直接在串列裡做排序。為了提醒你這件事,他不會回傳排序過的串列。這樣一來,當你需要排序過和未排序過的串列時,你就不會被誤導而不小心覆蓋掉串列。

如果你想要他回傳新的串列,那可以改用內建的 sorted()。他會用提供的可疊代物件 (iterable) 來排序建立新串列,並回傳之。例如,以下這個範例會說明如何有序地疊代字典的鍵:

for key in sorted(mydict):
    ...  # do whatever with mydict[key]...

如何在 Python 中指定和強制使用一個介面規範 (interface spec)?

像是 C++ 和 Java 等語言提供了模組的介面規範,他描述了該模組的方法和函式的原型。很多人認為這種在編譯時強制執行的介面規範在建構大型程式時十分有幫助。

Python 2.6 加入了 abc 模組,讓你可以定義抽象基底類別 (Abstract Base Class, ABC)。你可以使用 isinstance()issubclass() 來確認一個實例或是類別是否實作了某個抽象基底類別。而 collections.abc 模組定義了一系列好用的抽象基底類別,像是 IterableContainerMutableMapping

對 Python 來說,很多介面規範的優點可以用對元件適當的測試規則來達到。

一個針對模組的好測試套件提供了回歸測試 (regression testing),並作為模組介面規範和一組範例。許多 Python 模組可以直接當成腳本執行,並提供簡單的「自我測試」。即便模組使用了複雜的外部介面,他依然可以用外部介面的簡單的「樁」(stub) 模擬來獨立測試。doctestunittest 模組或第三方的測試框架可以用來建構詳盡徹底的測試套件來測試模組裡的每一行程式碼。

就像介面規範一樣,一個適當的測試規則在建造大型又複雜的 Python 應用程式時可以幫上忙。事實上,他可能可以有更好的表現,因為介面規範無法測試程式的特定屬性。舉例來說,append() 方法應該要在某個內部的串列最後面加上新的元素,而介面規範沒辦法測試你的 append() 是不是真的有正確的實作,但這在測試套件裡是件很簡單的事。

撰寫測試套件相當有幫助,而你會像要把程式碼設計成好測試的樣子。測試驅動開發 (test-driven development) 是一個越來越受歡迎的設計方法,他要求先完成部分的測試套件,再去撰寫真的要用的程式碼。當然 Python 也允許你草率地不寫任何測試。

為何沒有 goto 語法?

在 1970 年代,人們了解到沒有限制的 goto 會導致混亂、難以理解和修改的「義大利麵」程式碼 ("spaghetti" code)。在高階語言裡,這也是不需要的,因為有方法可以做邏輯分支(以 Python 來說,用 if 陳述式和 orandif-else 運算式)和迴圈(用 whilefor 陳述式,可能會有 continuebreak)。

我們也可以用例外來做「結構化的 goto」,這甚至可以跨函式呼叫。很多人覺得例外可以方便地模擬在 C、Fortran 和其他語言裡各種合理使用的「go」和「goto」。例如:

class label(Exception): pass  # declare a label

try:
    ...
    if condition: raise label()  # goto label
    ...
except label:  # where to goto
    pass
...

這依然不能讓你跳進迴圈內,這通常被認為是對 goto 的濫用。請小心使用。

為何純字串 (r-string) 不能以反斜線結尾?

更精確地來說,他不能以奇數個反斜線結尾:尾端未配對的反斜線會使結尾的引號被轉義 (escapes),變成一個未結束的字串。

設計出純字串是為了提供有自己反斜線轉義處理的處理器(主要是正規表示式)一個方便的輸入方式。這種處理器會把未配對的結尾反斜線當成錯誤,所以純字串不允許如此。相對地,他讓你用一個反斜線轉義引號。這些規則在他們預想的目的上正常地運作。

如果你嘗試建立 Windows 的路徑名稱,請注意 Windows 系統指令也接受一般斜線:

f = open("/mydir/file.txt")  # works fine!

如果你嘗試建立 DOS 指令的路徑名稱,試試看使用以下的範例:

dir = r"\this\is\my\dos\dir" "\\"
dir = r"\this\is\my\dos\dir\ "[:-1]
dir = "\\this\\is\\my\\dos\\dir\\"

為何 Python 沒有屬性賦值的 with 陳述式?

Python 的 with 陳述式包裝了一區塊程式的執行,在進入和離開該區塊時執行程式碼。一些語言會有像如下的結構:

with obj:
    a = 1               # equivalent to obj.a = 1
    total = total + 1   # obj.total = obj.total + 1

但在 Python,這種結構是模糊的。

在其他語言裡,像是 Object Pascal、Delphi 和 C++,使用的是靜態型別,所以我們可以清楚地知道是哪一個成員被指派值。這是靜態型別的重點 — 在編譯的時候,編譯器永遠都知道每個變數的作用域 (scope)。

Python 使用的是動態型別。所以我們不可能提前知道在執行時哪個屬性會被使用到。成員屬性可能在執行時從物件中被新增或移除。這使得如果簡單來看的話,我們無法得知以下哪個屬性會被使用:區域的、全域的、或是成員屬性?

以下列不完整的程式碼為例:

def foo(a):
    with a:
        print(x)

這段程式碼假設「a」有一個叫做「x」的成員屬性。然後,Python 裡並沒有任何跡象告訴直譯器這件事。在假設「a」是一個整數的話,那會發生什麼事?如果有一個全域變數稱為「x」,那在這個 with 區塊會被使用嗎?如你所見,Python 動態的天性使得這種選擇更加困難。

然而,with 陳述式或類似的語言特性(減少程式碼量)的主要好處可以透過賦值來達成。相較於這樣寫:

function(args).mydict[index][index].a = 21
function(args).mydict[index][index].b = 42
function(args).mydict[index][index].c = 63

應該寫成這樣:

ref = function(args).mydict[index][index]
ref.a = 21
ref.b = 42
ref.c = 63

這也有提升執行速度的副作用,因為 Python 的名稱綁定解析會在執行的時候發生,而第二版只需要執行解析一次即可。

為何產生器 (generator) 不支援 with 陳述式?

出於技術原因,把產生器直接用作情境 (context) 管理器會無法正常運作。因為通常來說,產生器是被當成疊代器 (iterator),到最後完成時不需要被手動關閉。但如果你需要的話,你可以在 with 陳述式裡用「contextlib.closing(generator)」來包裝他。

為何 if、while、def、class 陳述式裡需要冒號?

需要冒號主要是為了增加可讀性(由 ABC 語言的實驗得知)。試想如下範例:

if a == b
    print(a)

以及:

if a == b:
    print(a)

注意第二個例子稍微易讀一些的原因。可以更進一步觀察,一個冒號是如何放在這個 FAQ 答案的例子裡的,這是標準的英文用法。

另一個小原因是冒號會使編輯器更容易做語法突顯,他們只需要看冒號的位置就可以決定是否需要更多縮排,而不用做更多繁複精密的程式碼剖析。

為何 Python 允許在串列和元組末端加上逗號?

Python 允許你在串列、元組和字典的結尾加上逗號:

[1, 2, 3,]
('a', 'b', 'c',)
d = {
    "A": [1, 5],
    "B": [6, 7],  # last trailing comma is optional but good style
}

這有許多原因可被允許。

當你要把串列、元組或字典的值寫成多行時,這樣做會讓你新增元素時較為方便,因為你不需要在前一行加上逗號。這幾行的值也可以被重新排序,而不會導致語法錯誤。

不小心遺漏了逗號會導致難以發現的錯誤,例如:

x = [
  "fee",
  "fie"
  "foo",
  "fum"
]

這個串列看起來有四個元素,但他其實只有三個:「fee」、「fiefoo」、「fum」。永遠記得加上逗號以避免這種錯誤。

允許結尾逗號也讓生成的程式碼更容易產生。