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__ 的文档。
 

你可能感兴趣的:(Python Cookbook 第二版 汉化版 [Recipe 1.8] 检测字符串是否包含特定的字符集合)