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

15. 為什麼 CPython 不使用更傳統的垃圾回收方案?

首先,這不是 C 標準特性,因此不能移植。(是的,我們知道 Boehm GC 庫。它包含了 大多數 常見平臺(但不是所有平臺)的彙編代碼,儘管它基本上是透明的,但也不是完全透明的; 要讓 Python 使用它,需要使用補丁。)

當 Python 嵌入到其他應用程序中時,傳統的 GC 也成為一個問題。在獨立的 Python 中,可以用 GC 庫提供的版本替換標準的 malloc()和 free(),嵌入 Python 的應用程序可能希望用 它自己 替代 malloc()和 free(),而可能不需要 Python 的。現在,CPython 可以正確地實現 malloc()和 free()。

16. CPython 退出時為什麼不釋放所有內存?

當 Python 退出時,從全局命名空間或 Python 模塊引用的對象並不總是被釋放。如果存在循環引用,則可能發生這種情況 C 庫分配的某些內存也是不可能釋放的(例如像 Purify 這樣的工具會抱怨這些內容)。但是,Python 在退出時清理內存並嘗試銷燬每個對象。

如果要強制 Python 在釋放時刪除某些內容,請使用 atexit 模塊運行一個函數,強制刪除這些內容。

17. 為什麼有單獨的元組和列表數據類型?

雖然列表和元組在許多方面是相似的,但它們的使用方式通常是完全不同的。可以認為元組類似於 Pascal 記錄或 C 結構;它們是相關數據的小集合,可以是不同類型的數據,可以作為一個組進行操作。例如,笛卡爾座標適當地表示為兩個或三個數字的元組。

另一方面,列表更像其他語言中的數組。它們傾向於持有不同數量的對象,所有對象都具有相同的類型,並且逐個操作。例如, os.listdir('.') 返回表示當前目錄中的文件的字符串列表。如果向目錄中添加了一兩個文件,對此輸出進行操作的函數通常不會中斷。

元組是不可變的,這意味著一旦創建了元組,就不能用新值替換它的任何元素。列表是可變的,這意味著您始終可以更改列表的元素。只有不變元素可以用作字典的 key,因此只能將元組和非列表用作 key。

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

18. 列表如何在 CPython 中實現?

CPython 的列表實際上是可變長度的數組,而不是 lisp 風格的鏈表。該實現使用對其他對象的引用的連續數組,並在列表頭結構中保留指向該數組和數組長度的指針。

這使得索引列表 a[i] 的操作成本與列表的大小或索引的值無關。

當添加或插入項時,將調整引用數組的大小。並採用了一些巧妙的方法來提高重複添加項的性能; 當數組必須增長時,會分配一些額外的空間,以便在接下來的幾次中不需要實際調整大小。

19. 字典如何在 CPython 中實現?

CPython 的字典實現為可調整大小的哈希表。與 B-樹相比,這在大多數情況下為查找(目前最常見的操作)提供了更好的性能,並且實現更簡單。

字典的工作方式是使用 hash() 內置函數計算字典中存儲的每個鍵的 hash 代碼。hash 代碼根據鍵和每個進程的種子而變化很大;例如,"Python" 的 hash 值為-539294296,而"python"(一個按位不同的字符串)的 hash 值為 1142331976。然後,hash 代碼用於計算內部數組中將存儲該值的位置。假設您存儲的鍵都具有不同的 hash 值,這意味著字典需要恆定的時間 -- O(1),用 Big-O 表示法 -- 來檢索一個鍵。

20. 為什麼字典 key 必須是不可變的?

字典的哈希表實現使用從鍵值計算的哈希值來查找鍵。如果鍵是可變對象,則其值可能會發生變化,因此其哈希值也會發生變化。但是,由於無論誰更改鍵對象都無法判斷它是否被用作字典鍵值,因此無法在字典中修改條目。然後,當你嘗試在字典中查找相同的對象時,將無法找到它,因為其哈希值不同。如果你嘗試查找舊值,也不會找到它,因為在該哈希表中找到的對象的值會有所不同。

如果你想要一個用列表索引的字典,只需先將列表轉換為元組;用函數 tuple(L)創建一個元組,其條目與列表 L相同。元組是不可變的,因此可以用作字典鍵。

已經提出的一些不可接受的解決方案:

  • 哈希按其地址(對象 ID)列出。這不起作用,因為如果你構造一個具有相同值的新列表,它將無法找到;例如:
mydict = {[1, 2]: '12'}
print(mydict[[1, 2]])
  • 會引發一個 KeyError 異常,因為第二行中使用的 [1, 2] 的 id 與第一行中的 id 不同。換句話說,應該使用 == 來比較字典鍵,而不是使用is 。
  • 使用列表作為鍵時進行復制。這沒有用的,因為作為可變對象的列表可以包含對自身的引用,然後複製代碼將進入無限循環。
  • 允許列表作為鍵,但告訴用戶不要修改它們。當你意外忘記或修改列表時,這將產生程序中的一類難以跟蹤的錯誤。它還使一個重要的字典不變量無效:d.keys() 中的每個值都可用作字典的鍵。
  • 將列表用作字典鍵後,應標記為其只讀。問題是,它不僅僅是可以改變其值的頂級對象;你可以使用包含列表作為鍵的元組。將任何內容作為鍵關聯到字典中都需要將從那裡可到達的所有對象標記為只讀 —— 並且自引用對象可能會導致無限循環。

如果需要,可以使用以下方法來解決這個問題,但使用它需要你自擔風險:你可以將一個可變結構包裝在一個類實例中,該實例同時具有 __eq__() 和 __hash__() 方法。然後,你必須確保駐留在字典(或其他基於 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

注意,哈希計算由於列表的某些成員可能不可用以及算術溢出的可能性而變得複雜。

此外,必須始終如此,如果 o1 == o2 (即 o1.__eq__(o2) is True )則 hash(o1) == hash(o2)``(即``o1.__hash__() == o2.__hash__() ),無論對象是否在字典中。如果你不能滿足這些限制,字典和其他基於 hash 的結構將會出錯。

對於 ListWrapper ,只要包裝器對象在字典中,包裝列表就不能更改以避免異常。除非你準備好認真考慮需求以及不正確地滿足這些需求的後果,否則不要這樣做。請留意。

21. 為什麼 list.sort() 沒有返回排序列表?

在性能很重要的情況下,僅僅為了排序而複製一份列表將是一種浪費。因此, list.sort() 對列表進行了適當的排序。為了提醒您這一事實,它不會返回已排序的列表。這樣,當您需要排序的副本,但也需要保留未排序的版本時,就不會意外地覆蓋列表。

如果要返回新列表,請使用內置 sorted() 函數。此函數從提供的可迭代列表中創建新列表,對其進行排序並返回。例如,下面是如何迭代遍歷字典並按 keys 排序:

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

22. 如何在 Python 中指定和實施接口規範?

由 C++和 Java 等語言提供的模塊接口規範描述了模塊的方法和函數的原型。許多人認為接口規範的編譯時強制執行有助於構建大型程序。

Python 2.6 添加了一個 abc 模塊,允許定義抽象基類 (ABCs)。然後可以使用isinstance() 和 issubclass() 來檢查實例或類是否實現了特定的 ABC。collections.abc 模塊定義了一組有用的 ABCs 例如 Iterable , Container , 和 MutableMapping

對於 Python,通過對組件進行適當的測試規程,可以獲得接口規範的許多好處。還有一個工具 PyChecker,可用於查找由於子類化引起的問題。

一個好的模塊測試套件既可以提供迴歸測試,也可以作為模塊接口規範和一組示例。許多 Python 模塊可以作為腳本運行,以提供簡單的“自我測試”。即使是使用複雜外部接口的模塊,也常常可以使用外部接口的簡單“樁代碼(stub)”模擬進行隔離測試。可以使用 doctest 和 unittest 模塊或第三方測試框架來構造詳盡的測試套件,以運行模塊中的每一行代碼。

適當的測試規程可以幫助在 Python 中構建大型的、複雜的應用程序以及接口規範。事實上,它可能會更好,因為接口規範不能測試程序的某些屬性。例如,append() 方法將向一些內部列表的末尾添加新元素;接口規範不能測試您的 append() 實現是否能夠正確執行此操作,但是在測試套件中檢查這個屬性是很簡單的。

編寫測試套件非常有用,您可能希望設計代碼時著眼於使其易於測試。一種日益流行的技術是面向測試的開發,它要求在編寫任何實際代碼之前,首先編寫測試套件的各個部分。當然,Python 允許您草率行事,根本不編寫測試用例。

23. 為什麼沒有 goto?

可以使用異常捕獲來提供 “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。謹慎使用。

24. 為什麼原始字符串(r-strings)不能以反斜槓結尾?

更準確地說,它們不能以奇數個反斜槓結束:結尾處的不成對反斜槓會轉義結束引號字符,留下未結束的字符串。

原始字符串的設計是為了方便想要執行自己的反斜槓轉義處理的處理器(主要是正則表達式引擎)創建輸入。此類處理器將不匹配的尾隨反斜槓視為錯誤,因此原始字符串不允許這樣做。反過來,允許通過使用引號字符轉義反斜槓轉義字符串。當 r-string 用於它們的預期目的時,這些規則工作的很好。

如果您正在嘗試構建 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\\\\"

25. 為什麼 Python 沒有屬性賦值的“with”語句?

Python 有一個 'with' 語句,它封裝了塊的執行,在塊的入口和出口調用代碼。有些語言的結構是這樣的:

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

在 Python 中,這樣的結構是不明確的。

其他語言,如 ObjectPascal、Delphi 和 C++ 使用靜態類型,因此可以毫不含糊地知道分配給什麼成員。這是靜態類型的要點 -- 編譯器 總是 在編譯時知道每個變量的作用域。

Python 使用動態類型。事先不可能知道在運行時引用哪個屬性。可以動態地在對象中添加或刪除成員屬性。這使得無法通過簡單的閱讀就知道引用的是什麼屬性:局部屬性、全局屬性還是成員屬性?

例如,採用以下不完整的代碼段:

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

該代碼段假設 "a" 必須有一個名為 "x" 的成員屬性。然而,Python 中並沒有告訴解釋器這一點。假設 "a" 是整數,會發生什麼?如果有一個名為 "x" 的全局變量,它是否會在 with 塊中使用?如您所見,Python 的動態特性使得這樣的選擇更加困難。

然而,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 在運行時解析名稱綁定,而第二個版本只需要執行一次解析。

26. 為什麼 if/while/def/class 語句需要冒號?

冒號主要用於增強可讀性(ABC 語言實驗的結果之一)。考慮一下這個:

if a == b
print(a)

if a == b:
print(a)

注意第二種方法稍微容易一些。請進一步注意,在這個 FAQ 解答的示例中,冒號是如何設置的;這是英語中的標準用法。

另一個次要原因是冒號使帶有語法突出顯示的編輯器更容易工作;他們可以尋找冒號來決定何時需要增加縮進,而不必對程序文本進行更精細的解析。

27. 為什麼 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" 。總是加上逗號可以避免這個錯誤的來源。

允許尾隨逗號也可以使編程代碼更容易生成。


分享到:


相關文章: