Python Cookbook 第二版 漢化版 [Recipe 1.8] 檢測字符串是否包含特定的字符集合

Recipe 1.8. Checking Whether a String Contains a Set of Characters
Recipe 1.8. 檢測字符串是否包含特定的字符集合


Credit: Jürgen Hermann, Horst Hansen


問題

您須要檢查字符串中是否出現了特定的字符集合。


解法

最簡單的解法清晰、快捷、通用(不僅適用於字符串,還適用於任何序列;不僅適用於集合,還適用於任何您可以對其進行 membership 測試的容器):

def containsAny(seq, aset):
    """ Check whether sequence seq contains ANY of the items in aset. """
    for c in seq:
        if c in aset: return True
    return False

採用更高級、更復雜的解法可換得一些速度優勢,利用標準庫模塊 itertools 以基本相同的思路來處理:

import itertools
def containsAny(seq, aset):
    for item in itertools.ifilter(aset.__contains__, seq):
        return True
    return False


討論

大多數與集合(set)相關的問題最好是用 Python 2.4 引入的內建 set 類型來解決(在 Python 2.3 中您可以使用等效的標準庫中的 sets.Set 類型)。但這其中也有例外。如下例所示,純粹基於 set 的方案可以是:

def containsAny(seq, aset):
    return bool(set(aset).intersection(seq))

然而,此方案中的 seq 的全部項目都必須被檢查。而本條目“解法”欄目中的函數採用的是“短路(short-circuit)”手法:一旦找到就立刻返回。當然,若結果爲 False ,“解法”欄目中的函數仍然必須檢查 seq 的全部項目——否則我們就無法確認 seq 中的每個項目都不在 aset 中。而在結果爲 True 的情況下,我們經常能夠很快地明確結果,因爲只須找到某一項是 aset 的成員即可。當然,上述情況是否值得斟酌,完全取決於數據的具體情況。若 seq 很短或者結果大多是 False ,那麼上述兩種方案就沒有實質上的區別;而對於很長的 seq 來說(通常可以很快地明確結果爲 True),這區別就極爲重要了。

“解法”欄目中 containsAny 的第一個版本的優點是簡單、明晰,以一目瞭然的方式表達了核心思路。第二個版本可能顯得“機巧”,而在 Python 世界中“機巧”並不是個褒義詞,因爲 Python 世界的核心價值觀是簡單和明晰。然而,第二個版本還是值得斟酌,因爲它展示了一種基於標準庫模塊 itertools 的更高級的方案,而更高級的方案往往勝過較低級的方案(儘管在本條目中這一點頗具爭議)。itertools.ifilter 接收一個 predicate(謂詞)和一個 iterable(可迭代體),並將 iterable 中滿足 predicate 的項目 yield 出來。這裏將 anyset.__contains__ 作爲 predicate ;當我們撰寫 in anyset 形式的語句來進行 membership 測試時,anyset.__contains__ 就是被綁定的方法(bound method),語句內部會調用它。因此,只要 seq 中有哪個項目是屬於 anyset ,ifilter 就會將其 yield 出來;一旦發生這種情況,我們就可以立刻返回 True 。如果代碼執行到了 for 語句之後,就必然意味着 return True 從未被執行過,因爲 seq 的任何一個項目都不屬於 anyset ,由此應該 return False


---- BOX BEGIN ----
什麼是“Predicate(謂詞)”?
“Predicate(謂詞)”是討論編程時您會經常遇到的一個術語,意即“返回 True False 的函數(或其他可調用對象)”。若 predicate 返回結果爲真,就稱滿足了 predicate 。
---- BOX E N D ----

若您的應用程序需要諸如 containsAny 這樣的函數來檢查一個字符串(或其他序列)是否包含某集合的成員,您可能還需要象下面這樣的變體形式:

def containsOnly(seq, aset):
    """ Check whether sequence seq contains ONLY items in aset. """
    for c in seq:
        if c not in aset: return False
    return True

containsOnly containsAny 形式相同,只是在邏輯上反過來了。其他明顯類似的功能本質上要求檢查所有項目,無法應用“短路”手法,因此最好使用(Python 2.4 中)內建的 set 類型來處理(Python 2.3 中可使用 sets.Set ,用法相同):

def containsAll(seq, aset):
    """ Check whether sequence seq contains ALL the items in aset. """
    return not set(aset).difference(seq)

若您還沒用慣 set(或 sets.Set)的 difference 方法,請注意該方法的語義:對於任意集合 a ,a.difference(b) 返回 a 中所有不屬於 b 的元素的集合(如同 a-set(b))。例如:

>>> L1 = [1, 2, 3, 3]
>>> L2 = [1, 2, 3, 4]
>>> set(L1).difference(L2)
set([  ])
>>> set(L2).difference(L1)
set([4])

希望上述例子有助於理解如下事實:

>>> containsAll(L1, L2)
False
>>> containsAll(L2, L1)
True

(換句話說,請不要將 difference set 的另一個方法 symmetric_difference 搞混淆了,symmetric_difference 返回 a 和 b 中所有“屬於 a 但不屬於 b,或者屬於 b 但不屬於 a”的元素的集合)

[譯註] 關於 symmetric_difference 請參考如下例子:
>>> L1 = [1, 2, 3, 5]
>>> L2 = [1, 3, 4, 8]
>>> set(L1).symmetric_difference(L2)
set([2, 4, 5, 8])
>>> set(L2).symmetric_difference(L1)
set([2, 4, 5, 8])

若您要處理的 seq aset 只是(單純的,而非 Unicode)字符串,可能就不完全需要本條目提供的函數所具有的通用性,不妨考慮採用 Recipe 1.10 中講到的更有針對性地方案(該方案基於字符串的 translate 方法以及標準庫中的 string.maketrans 函數)。例如:

import string
notrans = string.maketrans('', '')           # identity "translation"
def containsAny(astr, strset):
    return len(strset) != len(strset.translate(notrans, astr))
def containsAll(astr, strset):
    return not strset.translate(notrans, astr)

這個方案略顯巧妙,其原理在於:strset.translate(notrans, astr) 是由 strset 中那些“不屬於 astr 的字符”所組成的子序列。若這個子序列與 strset 長度相同,那就說明 strset.translate 並沒有移除 strset 中任何字符,因此也就說明在 strset 中沒有字符是屬於 astr 的。反之,若子序列爲空,那就說明 strset.translate 移除了 strset 中所有的字符,因此也就說明 strset 中所有的字符也都在 astr 中。當您想要將字符串作爲字符集合來對待時,自然而然地就會用到 translate 方法,因爲該方法效率高,用起來順手,還具伸縮性(詳情請參見 Recipe 1.10)。

本條目的這兩組解決方案具有非常不同的通用性。前一組方案非常通用,並不僅限於字符串處理,對操作對象的要求相當少。而後一組基於 tanslate 方法的方案僅在“astr strset 都是字符串”或“astr strset 的功能在表象上與普通字符串非常接近”的情況下才能湊效。Unicode 字符串不適用於基於 tanslate 方法的方案,因爲 Unicode 字符串的 translate 方法與普通字符串的 translate 方法的簽名式(signature)不一樣——Unicode 字符串的 translate 方法只有一個參數(該參數爲 dict 對象,其將代碼數字映射至 Unicode 字符串或 None),而普通字符串的 translate 方法有兩個參數(都是字符串)。


請參見

Recipe 1.10 ;Library Reference 和 Python in a Nutshell 中關於字符串及 Unicode 對象的 translate 方法,以及 string 模塊的 maketrans 函數的文檔;Library Reference 和 Python in a Nutshell 中關於內建 set 類型(僅限 Python 2.4 及後續版本)、sets itertools 模塊,以及特殊方法 __contains__ 的文檔。
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章