Python乾貨(一):27個問題,告訴你 Python 為什麼如此設計?


Python乾貨(一):27個問題,告訴你 Python 為什麼如此設計?

01. 為什麼使用縮進來分組語句?

Guido van Rossum 認為使用縮進進行分組非常優雅,並且大大提高了普通 Python 程序的清晰度。大多數人在一段時間後就學會並喜歡上這個功能。

由於沒有開始/結束括號,因此解析器感知的分組與人類讀者之間不會存在分歧。偶爾 C 程序員會遇到像這樣的代碼片段:

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

如果條件為真,則只執行 x++ 語句,但縮進會使你認為情況並非如此。即使是經驗豐富的 C 程序員有時會長時間盯著它,想知道為什麼即使 x > y , y 也在減少。

因為沒有開始/結束括號,所以 Python 不太容易發生編碼式衝突。在 C 中,括號可以放到許多不同的位置。如果您習慣於閱讀和編寫使用一種風格的代碼,那麼在閱讀(或被要求編寫)另一種風格時,您至少會感到有些不安。

許多編碼風格將開始/結束括號單獨放在一行上。這使得程序相當長,浪費了寶貴的屏幕空間,使得更難以對程序進行全面的瞭解。理想情況下,函數應該適合一個屏幕(例如,20--30 行)。20 行 Python 可以完成比 20 行 C 更多的工作。這不僅僅是由於缺少開始/結束括號 -- 缺少聲明和高級數據類型也是其中的原因 -- 但縮進基於語法肯定有幫助。

02. 為什麼簡單的算術運算得到奇怪的結果?

請看下一個問題。

03. 為什麼浮點計算不準確?

用戶經常對這樣的結果感到驚訝:

>>> 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.0011001100110011001100110011001100110011001100110011 (binary)

它對應於十進制數值:

1.1999999999999999555910790149937383830547332763671875 (decimal)

典型的 53 位精度為 Python 浮點數提供了 15-16 位小數的精度。

要獲得更完整的解釋,請參閱 Python 教程中的 浮點算術 一章。

04. 為什麼 Python 字符串是不可變的?

有幾個優點。

一個是性能:知道字符串是不可變的,意味著我們可以在創建時為它分配空間,並且存儲需求是固定不變的。這也是元組和列表之間區別的原因之一。

另一個優點是,Python 中的字符串被視為與數字一樣“基本”。任何動作都不會將值 8 更改為其他值,在 Python 中,任何動作都不會將字符串 "8" 更改為其他值。

05. 為什麼必須在方法定義和調用中顯式使用“self”?

這個想法借鑑了 Modula-3 語言。出於多種原因它被證明是非常有用的。

首先,更明顯的顯示出,使用的是方法或實例屬性而不是局部變量。閱讀 self.x或 self.meth() 可以清楚地表明,即使您不知道類的定義,也會使用實例變量或方法。在 C++ 中,可以通過缺少局部變量聲明來判斷(假設全局變量很少見或容易識別) —— 但是在 Python 中沒有局部變量聲明,所以必須查找類定義才能確定。一些 C++ 和 Java 編碼標準要求實例屬性具有 m_ 前綴,因此這種顯式性在這些語言中仍然有用。

其次,這意味著如果要顯式引用或從特定類調用該方法,不需要特殊語法。在 C++ 中,如果你想使用在派生類中重寫基類中的方法,你必須使用 :: 運算符 -- 在 Python 中你可以編寫 baseclass.methodname(self, <argumentlist>)。這對於 __init__() 方法非常有用,特別是在派生類方法想要擴展同名的基類方法,而必須以某種方式調用基類方法時。/<argumentlist>

最後,它解決了變量賦值的語法問題:為了 Python 中的局部變量(根據定義!)在函數體中賦值的那些變量(並且沒有明確聲明為全局)賦值,就必須以某種方式告訴解釋器一個賦值是為了分配一個實例變量而不是一個局部變量,它最好是通過語法實現的(出於效率原因)。C++ 通過聲明來做到這一點,但是 Python 沒有聲明,僅僅為了這個目的而引入它們會很可惜。使用顯式的 self.var 很好地解決了這個問題。類似地,對於使用實例變量,必須編寫 self.var 意味著對方法內部的非限定名稱的引用不必搜索實例的目錄。換句話說,局部變量和實例變量存在於兩個不同的命名空間中,您需要告訴 Python 使用哪個命名空間。

06. 為什麼不能在表達式中賦值?

許多習慣於 C 或 Perl 的人抱怨,他們想要使用 C 的這個特性:

while (line = readline(f)) {
// do something with line
}

但在 Python 中被強制寫成這樣:

while True:
line = f.readline()
if not line:
break
... # do something with line

不允許在 Python 表達式中賦值的原因是這些其他語言中常見的、很難發現的錯誤,是由這個結構引起的:

if (x = 0) {
// error handling
}
else {
// code that only works for nonzero x
}

錯誤是一個簡單的錯字:x = 0 ,將 0 賦給變量 x ,而比較 x == 0 肯定是可以預期的。

已經有許多替代方案提案。大多數是為了少打一些字的黑客方案,但使用任意或隱含的語法或關鍵詞,並不符合語言變更提案的簡單標準:它應該直觀地向尚未被介紹到這一概念的人類讀者提供正確的含義。

一個有趣的現象是,大多數有經驗的 Python 程序員都認識到 while True 的習慣用法,也不太在意是否能在表達式構造中賦值; 只有新人表達了強烈的願望希望將其添加到語言中。

有一種替代的拼寫方式看起來很有吸引力,但通常不如"while True"解決方案可靠:

line = f.readline()
while line:
... # do something with line...
line = f.readline()

問題在於,如果你改變主意(例如你想把它改成 sys.stdin.readline() ),如何知道下一行。你必須記住改變程序中的兩個地方 -- 第二次出現隱藏在循環的底部。

最好的方法是使用迭代器,這樣能通過 for 語句來循環遍歷對象。例如 file objects 支持迭代器協議,因此可以簡單地寫成:

for line in f:
... # do something with line...

07 為什麼 Python 對某些功能(例如 list.index())使用方法來實現,而其他功能(例如 len(List))使用函數實現?

正如 Guido 所說:

(a) 對於某些操作,前綴表示法比後綴更容易閱讀 -- 前綴(和中綴!)運算在數學中有著悠久的傳統,就像在視覺上幫助數學家思考問題的記法。比較一下我們將 x*(a+b) 這樣的公式改寫為 x*a+x*b 的容易程度,以及使用原始 OO 符號做相同事情的笨拙程度。

(b) 當讀到寫有 len(X)的代碼時,就知道它要求的是某件東西的長度。這告訴我們兩件事:結果是一個整數,參數是某種容器。相反,當閱讀 x.len()時,必須已經知道 x 是某種實現接口的容器,或者是從具有標準 len()的類繼承的容器。當沒有實現映射的類有 get()或 key()方法,或者不是文件的類有 write()方法時,我們偶爾會感到困惑。—https://mail.python.org/pipermail/python-3000/2006-November/004643.html

08. 為什麼 join()是一個字符串方法而不是列表或元組方法?

從 Python 1.6 開始,字符串變得更像其他標準類型,當添加方法時,這些方法提供的功能與始終使用 String 模塊的函數時提供的功能相同。這些新方法中的大多數已被廣泛接受,但似乎讓一些程序員感到不舒服的一種方法是:

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

結果如下:

"1, 2, 4, 8, 16"

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

第一條是這樣的:“使用字符串文本(String Constant)的方法看起來真的很難看”,答案是也許吧,但是字符串文本只是一個固定值。如果在綁定到字符串的名稱上允許使用這些方法,則沒有邏輯上的理由使其在文字上不可用。

第二個異議通常是這樣的:“我實際上是在告訴序列使用字符串常量將其成員連接在一起”。遺憾的是並非如此。出於某種原因,把 split() 作為一個字符串方法似乎要容易得多,因為在這種情況下,很容易看到:

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

是對字符串文本的指令,用於返回由給定分隔符分隔的子字符串(或在默認情況下,返回任意空格)。

join() 是字符串方法,因為在使用該方法時,您告訴分隔符字符串去迭代一個字符串序列,並在相鄰元素之間插入自身。此方法的參數可以是任何遵循序列規則的對象,包括您自己定義的任何新的類。對於字節和字節數組對象也有類似的方法。

09. 異常有多快?

如果沒有引發異常,則 try/except 塊的效率極高。實際上捕獲異常是昂貴的。在 2.0 之前的 Python 版本中,通常使用這個習慣用法:

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

只有當你期望 dict 在任何時候都有 key 時,這才有意義。如果不是這樣的話,你就是應該這樣編碼:

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

對於這種特定的情況,您還可以使用 value = dict.setdefault(key, getvalue(key)),但前提是調用 getvalue()足夠便宜,因為在所有情況下都會對其進行評估。

10. 為什麼 Python 中沒有 switch 或 case 語句?

你可以通過一系列 if... elif... elif... else.輕鬆完成這項工作。對於 switch 語句語法已經有了一些建議,但尚未就是否以及如何進行範圍測試達成共識。有關完整的詳細信息和當前狀態,請參閱

PEP 275

對於需要從大量可能性中進行選擇的情況,可以創建一個字典,將 case 值映射到要調用的函數。例如:

def function_1(...):
...
functions = {'a': function_1,
'b': function_2,
'c': self.method_1, ...}
func = functions[value]
func()

對於對象調用方法,可以通過使用 getattr() 內置檢索具有特定名稱的方法來進一步簡化:

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

建議對方法名使用前綴,例如本例中的 visit_ 。如果沒有這樣的前綴,如果值來自不受信任的源,攻擊者將能夠調用對象上的任何方法。

11. 難道不能在解釋器中模擬線程,而非得依賴特定於操作系統的線程實現嗎?

答案 1:不幸的是,解釋器為每個 Python 堆棧幀推送至少一個 C 堆棧幀。此外,擴展可以隨時回調 Python。因此,一個完整的線程實現需要對 C 的線程支持。

答案 2:幸運的是, Stackless Python 有一個完全重新設計的解釋器循環,可以避免 C 堆棧。

12. 為什麼 lambda 表達式不包含語句?

Python 的 lambda 表達式不能包含語句,因為 Python 的語法框架不能處理嵌套在表達式內部的語句。然而,在 Python 中,這並不是一個嚴重的問題。與其他語言中添加功能的 lambda 表單不同,Python 的 lambdas 只是一種速記符號,如果您懶得定義函數的話。

函數已經是 Python 中的第一類對象,可以在本地範圍內聲明。因此,使用 lambda 而不是本地定義的函數的唯一優點是你不需要為函數創建一個名稱 -- 這只是一個分配了函數對象(與 lambda 表達式生成的對象類型完全相同)的局部變量!

13. 可以將 Python 編譯為機器代碼,C 或其他語言嗎?

Cython 將帶有可選註釋的 Python 修改版本編譯到 C 擴展中。Nuitka 是一個將 Python 編譯成 C++ 代碼的新興編譯器,旨在支持完整的 Python 語言。要編譯成 Java,可以考慮 VOC 。

14. Python 如何管理內存?

Python 內存管理的細節取決於實現。Python 的標準實現 CPython 使用引用計數來檢測不可訪問的對象,並使用另一種機制來收集引用循環,定期執行循環檢測算法來查找不可訪問的循環並刪除所涉及的對象。gc 模塊提供了執行垃圾回收、獲取調試統計信息和優化收集器參數的函數。

但是,其他實現(如 Jython 或 PyPy ),)可以依賴不同的機制,如完全的垃圾回收器 。如果你的 Python 代碼依賴於引用計數實現的行為,則這種差異可能會導致一些微妙的移植問題。

在一些 Python 實現中,以下代碼(在 CPython 中工作的很好)可能會耗盡文件描述符:

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

實際上,使用 CPython 的引用計數和析構函數方案, 每個新賦值的 f 都會關閉前一個文件。然而,對於傳統的 GC,這些文件對象只能以不同的時間間隔(可能很長的時間間隔)被收集(和關閉)。

如果要編寫可用於任何 python 實現的代碼,則應顯式關閉該文件或使用 with語句;無論內存管理方案如何,這都有效:

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

篇幅有限,剩下的乾貨集錦會在下一期為大家更新!


分享到:


相關文章: