比較Python中的東西。這聽起來幾乎是不需要教的,但是我發現Python的比較運算符經常被Python新手誤解和低估。
我們來回顧一下Python的比較運算符如何處理不同類型的對象,然後看看如何使用這些比較運算符來提高代碼的可讀性。
Python中的比較運算符
我這裡所說的 “比較運算符”是指相等運算符(== 和 !=)和排序運算符(,>=)。
我們可以用這些運算符來比較數字,正如你所期望的:
除此之外,我們也可以用這些運算符來比較字符串:
甚至於元組:
許多編程語言都沒有與Python非常靈活的比較運算符等價的運算符。
稍後我們將看一看這些運算符如何處理元組和更復雜的對象,我們先從簡單一點的開始:字符串比較。
Python中的字符串比較
字符串的相等和不相等十分簡單。如果兩個字符串有完全相同的字符,那麼它們是相等的:
注意,我忽略了一個非常大的例外: unicode字符。通常有多種方法可以表示相同的文本,在將這些不同的表示視為相等之前,必須對它們進行標準化。為了簡單起見,本文將堅持使用ASCII字符。
字符串的排序是Python中比較有趣的部分:
字符串“pickle”比字符串“python”小,因為我們是按字母順序排序的…大小寫有一部分作用:
字符串“Python”小於“pickle”,因為P小於p。
這裡我們與其說是按照字母順序還不如說是按照ASCII- 碼順序排序的 (因為我們在python3中實際是使用unicode-碼)。這些字符串是按照它們的字符的ASCII碼值排序的(ASCII碼中p是112,而P是80)。
從技術上講,Python是比較這些字符的Unicode代碼點(這是ord函數所做的事情),而這恰好與比較ASCII字符的ASCII碼值結果相同。
字符串的排序規則是:
使用==操作符比較每個字符串的第n個字符(從第一個字符開始,索引為0);如果它們相等,則對下一個字符重複這個步驟
對於兩個不相等的字符,取具有較低代碼點的字符,並聲明其所在字符串“小於”另一個
如果所有字符都相等,那麼字符串也是相等的
如果一個字符串在步驟1中耗盡字符(一個字符串是另一個字符串的“前綴”),則較短的字符串“小於”較長的字符串
Python用於比較字符串的排序算法可能看起來很複雜,但它與字典中使用的排序算法非常相似;不是Python中的字典,而是物理字典(我們在互聯網出現之前使用的那些東西)。當在字典中對單詞排序時,我們優先考慮第一個字符,如果一個單詞是另一個單詞的前綴,那麼它就會排在前面。
元組的比較
我們可以問元組是否相等,就像我們可以問字符串是否相等一樣:
但是我們也可以使用排序運算符(,>=)來比較元組:
字符串排序可能有些直觀(我們大多數人在Python之前就已經學習了字母排序),但是元組排序一開始並不那麼直觀。但實際上你已經對元組排序有點熟悉了,因為元組排序和字符串排序使用相同的算法。
元組排序的規則(本質上與字符串排序相同):
使用==運算符比較每個元組的第n項(從第一個項開始,索引為0);如果它們相等,則對下一項重複這個步驟
對於兩個不相等的項,“小於”的項使包含它的元組也“小於”另一個元組
如果所有項都相等,則元組相等
如果一個元組在步驟1中耗盡了項(一個元組是另一個元組的“子集”),則較短的元組“小於”較長的元組
在Python中,這個算法看起來可能有點像這樣:
注意,我們永遠不會這樣編寫代碼,因為Python已經為我們完成了所有這些工作。上面整個函數與使用
字典排序
這種給予一個迭代中第一項優先權並類似於按字母順序的排序方式稱為字典排序。你不需要知道這個短語,但是如果你需要描述Python中排序的工作方式,就可以使用lexicographic這個詞。
字符串和元組是按字典順序排列的,正如我們所見,列表也是這樣:
實際上,Python中的大多數序列都應該按字典順序排列(range對象是一個例外,因為根本無法對它們進行排序)。
但並不是Python中的所有集合都依賴於字典排序。
字典和集合的比較
Python中的許多對象都可以進行相等比較,但不是都能夠排序。
例如,字典比較“相等”時,它們所有的鍵和值都相同:
但是字典不能使用運算符來排序:
集合也是類似的,除了集合可以使用排序運算符……它們只是不使用這些運算符來排序:
集合重載了這些運算符,以便回答關於一個集合是否是另一個集合的子集還是超集的問題(請參閱文檔中關於集合的部分)。
深度相等
Python中兩種數據結構之間的比較往往是深度比較。無論我們是在比較列表、元組、集合還是字典,當我們詢問其中兩個對象是否“相等”時,Python將遞歸遍歷每個子對象並詢問它們是否“相等”。
因此,給定一個字典就可以將其中的元組映射到元組列表:
詢問兩個字典是否相等等價於遞歸地詢問每個鍵值對是否相等:
字典會問它們的每個鍵“你在另一個字典裡嗎”,然後問這些鍵對應的每個值“你等於另一個值嗎”。但是,每一個操作都可能(就像在本例中)需要另一層深度的操作:鍵是需要遍歷的元組,而值是需要遍歷的列表。在這種情況下,需要更深入地遍歷這些值,即列表,因為它們包含更多的數據結構:元組。
不過,我們不必擔心這些:Python會自動地為我們做這些深入的比較。
雖然你不需要關心深度比較是怎樣進行的,但是,實際上Python的比較深度是輕易就能知道的。。
例如,如果我們有一個帶有x、y和z屬性的類,我們想要在我們的__eq__方法中進行比較,而不是使用這個冗長的布爾表達式:
我們可以將這些值處理成含有3個項的元組,來替代布爾表達式進行比較:
我發現這更易於閱讀,主要是因為我們在代碼中添加了對稱性:我們有一個==表達式,它的兩邊都有相同類型的對象。
深度排序
這種“深度比較”適用於相等比較,但也適用於排序。
深度排序的例子不如深度相等的例子明顯,但是確定哪些地方可以方便地進行深度排序可以幫助你極大地提高代碼的可讀性。
舉個例子方法:
這個 __lt__ 方法在其類上實現了
上面的__lt__方法會給予last_name優先權:只有當這兩個對象的last_name屬性恰好相等時才會檢查first_name。
如果我們想打破這個邏輯,我們可以這樣重寫我們的代碼:
或者,我們也可以使用元組的深度排序來代替:
在這裡,我們按照字典順序(首先按照它們的第一個項排序)排列元組。我們的元組正好包含字符串,這些字符串也會按字典順序排序(首先按其第一個字符排序)。因此,我們對這些對象進行了深度排序。
一次按多個屬性排序
在對Python對象排序時,瞭解Python序列的詞典排序和深度排序非常有用。從Python的角度來看,
排序實際上就是一遍又一遍地排列順序。Python內置的sorted函數接受一個key函數,它可以返回一個相應的key對象,並以此來對這些項進行排序。
這裡我們指定了一個key函數,它接受一個單詞並返回一個元組,該元組由兩部分組成:單詞的長度和大小寫規範化的單詞:
使用上面的key函數,我們可以先根據水果的長度排序,然後根據它們的大小寫標準化的等價項排序。所以“jujube”排在第一位,因為它是6個字母(比如longan 和 Loquat),但它按字母順序也是排在longan 和 Loquat之前。
如果我們只是按長度排序,我們會有一個不同的順序:
旁註:在Python中,深度比較實際上早於sorted 函數的key參數。在key函數出現之前,Python開發者會創建元組列表,對元組列表進行排序,然後從該列表中獲取他們關心的實際值(文檔中對此進行了討論)。
元組排序並不只是適用sorted函數。任何能看到key函數的地方都可以考慮使用元組排序。例如min和max函數:
在Python執行排序操作的任何地方,你都可以使用Python數據結構的深度排序。
深度哈希性 (和不可哈希性)
Python既具有深度相等性,又具有深度可排序性。但是Python的深度比較還不止於此:還有深度哈希性。
這主要是由元組帶來的的。元組可以用作字典中的鍵(正如我們前面看到的),它們可以在集合中使用:
但這隻適用於包含不可變值的元組:
包含列表的元組是不可哈希的,因為列表是不可哈希的:元組裡邊的每個對象都必須是可哈希的,這樣元組本身才是可哈希的。
因此,雖然包含列表的元組是不可哈希的,但是包含元組的元組是可哈希的:
元組通過分派給它們包含的項的哈希值來計算自身的哈希值:
雖然哈希性是一個很大的主題,但這就是我要說的全部。你不需要真正瞭解Python中的哈希過程是如何工作的,所以如果你發現這一部分令人困惑,也沒有關係!
這裡的要點是Python支持深度哈希,這就是我們可以使用元組作為字典鍵的原因,也是我們可以在集合中使用元組的原因。
深度比較是一種需要記住的工具
當你有一段以特定順序比較兩個基於子部分的對象的代碼時:
你可以優先考慮元組排序:
如果你正在進行很多東西的相等比較時:
你或許可以優先考慮深度相等:
如果你需要使用一個帶有由多個部分組成的鍵的字典時,並且這些部分都是可哈希的,你可以使用一個元組:
Python對詞典排序和深度比較的支持常常被來自其他編程語言的人忽視。請記住這些特性:你今天可能不需要它們,但在某個時候它們肯定會派上用場。
英文原文:https://treyhunner.com/2019/03/python-deep-comparisons-and-code-readability/譯者:天天向上
閱讀更多 Python部落 的文章