本节书摘来自异步社区《Python Cookbook(第2版)中文版》一书中的第1章,第1.8节,作者[美]Alex Martelli , Anna Martelli Ravenscrof , David Ascher ,高铁军 译,更多章节内容可以访问云栖社区“异步社区”公众号查看。
1.8 检查字符串中是否包含某字符集合中的字符
任务
检查字符串中是否出现了某字符集合中的字符。
解决方案
最简单的方法如下,兼具清晰、快速、通用(适用于任何序列,不仅仅是字符串,也适用于任何容器,不仅仅是集合):
def containsAny(seq, aset):
""" 检查序列seq是否含有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
讨论
对于大多数涉及集合的问题,我们最好使用Python 2.4中引入的内建类型set(如果还在使用Python 2.3,可以使用Python标准库中的等价的sets.Set类型)。然而,总是有例外的情况。比如,一个纯粹的基于集合的方法应该是像这样的:
def containsAny(seq, aset):
return bool(set(aset).intersection(seq))
不过这种方法就意味着seq中的每个成员都不可避免地要被检查。而本节方法中给出的函数,从某种角度讲,可以被叫做“短路法”:它一知道答案就迅速返回。如果答案是False,它必须检查seq中的每个子项—因为除非检查所有的子项,否则我们无法确保seq中不含有aset中的元素。不过如果答案是True,我们通常可以很快知道,因为只要找到一个子项是aset的成员就可以了。当然这通常是依赖于数据的。如果seq很短或者答案是False,实际上并没有多大区别,但如果seq很长,用哪种方式来检查就变得极其关键了(尤其是答案是True而且又可以很快探知结果的情况)。
本节给出的containsAny的第一个版本有着简洁和清晰的优点:它直观地表达了它背后蕴藏的思想。而第二个版本则可能显得有点“聪明”,这个词在Python的世界中可不是一个正面的表达赞美的形容词,因为简洁和清晰才是这个世界的核心价值。不过,第二个版本也有值得思考的地方,因为它展示了一种高级的、基于标准库的itertools模块的方法。大多数情况下高级方法总是比低级方法更好(当然在本节的这个特别的例子中,情况正好相反)。itertools.ifilter要求传入一个断定(译者注:Predicate,原意为断言、谓词或述词)和一个可迭代对象,然后筛选出可迭代对象中的满足该“断定”的描述的所有子项。这里,所谓的“断定”,我们用的是anyset. _contains ,当我们编写in anyset这样的代码来做检查的时候,其内部调用的就是anyset. contains _。所以,如果ifilter找到了什么东西,比如它找出一个seq的子项,同时也正是anyset的成员,函数就会立刻返回True。如果程序运行到了for语句之后,那肯定表示return True根本没有被执行,因为seq中的任何子项都不是anyset的成员,此时只能返回False。
什么是“断定”? 在一些编程的讨论中你常常可以看到这个词(predicate):意为一个返回True或False的函数(或者其他可调用对象)。如果它返回True,我们就称这个断定成立。
如果你的程序需要用到像containsAny这样的函数来检查一个字符串(或其他序列)是否包含了某个集合的成员,也可能会写出这样的变种:
def containsOnly(seq, aset):
""" 检查序列seq是否含有aset中的项 """
for c in seq:
if c not in aset: return False
return True
containsOnly和containsAny完全一样,只不过正好逻辑颠倒了一下。下面还有一个类似的例子,完全没办法短路(必须检查所有的子项),我们最好使用于内建的set类型(Python 2.4;或Python 2.3中的sets.Set,使用方法一样):
def containsAll(seq, aset):
""" 检查序列seq是否含有aset的所有的项 """
return not set(aset).difference(seq)
如果不习惯用set(或sets.Set)的方法difference,可以记住它的语义:任何一个set对象a,a.difference(b)(就像a-set(b))返回a中所有不属于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,它返回的集合包含了所有属于其中一个集合且不属于另一个集合的元素。
如果需要处理seq和aset中的字符串(非Unicode),可能不需要本节中这些通用的函数,而可以尝试更加特殊的方式,如第1.10节中的方法,基于字符串的方法translate和Python标准库中的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中任何一个字符都不属于astr。相反,如果子序列是空,说明所有的字符都被移除了,所以所有属于strset的字符也属于astr。当程序员们把字符串当做字符集合的时候,很自然地就会想到使用translate办法,因为这个方法速度不错,而且灵活易用,更多细节参看第1.10节。
不过本节中的两种方式的通用性完全不同。早先提出的方式通用性非常好:并不局限于字符串处理,它对你要处理的对象类型的要求也更少。而基于translate方法的方式则相反,它要求astr和strset都是普通字符串,或者在行为和功能上要与普通字符串非常相似。甚至连Unicode字符串都不行,因为Unicode字符串的translate方法的签名不同于普通字符串对应的translate版本—Unicode版本只需要一个参数(该参数是一个把码值映射到Unicode字符串或者None的dict对象),普通字符串版本则需要两个(必须都是普通字符串)。