Python食谱-1.8.检查字符串中是否包含一个集合中的字符

原文作者:Jurgen Hermann, Horst Hansen
中文编译:Tony(digitalsatori)

问题

如何检查一个集合(Set)中的任意一个字符是否包含在一个字符串中

解决方法

下面是一个简单,清晰,又通用的实现方法(它可以适用于任何“序列”,而不仅限于字符串,同时也支持任何可以检测其成员的容器对象,而不仅针对集合(Set):

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

讨论

大多数与集合相关的问题最好使用Python2.4以后引入的内置类型set。(如果你使用Python2.3你可以使用其标准库中等效的sets.Set类型)但是,凡事皆有例外。比如上面的问题用纯粹的基于set的方法来处理,会是这样的:

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

但是使用这种方法,seq中的每个成员都不可避免的要被全部测试。而在解决方法中的方法则会在找到答案后立即返回,不必遍历所有的成员。这对于处理一个非常长的序列来说就很重要了。

解决方法中的的第一个版本简单,清晰。第二个方法则显得“机巧”,在Python世界里这绝对不是一个赞誉之词。Python的核心价值体现在简单,清晰。但是,第二个版本也非常值得考虑。因为它是基于标准库中itertools模块的高阶实现方法。高阶实现方法多数情况下优于低阶实现方法(当然实际应用中还是要就事论事)。 itertools.ifilter 将一个‘判断‘(predicate)和一个可遍历对象(iterable)作为其参数,并且当'可遍历对象'中的成员满足'判断'时yield该成员对象。在本例中我们用 aset.__contains__ 来作为“判断”。所以,只要 ifilter 能yield任何值,它yield是seq中的某个成员对象,并且其同时也是aset中的成员。

什么是"判断“(Predicate)?

编程讨论中的术语,判断(Predicate):是指一个函数(或其他可被调用的对象(callables))只返回True或者False的结果。当返回True时,我们说这个判断被满足了。

我们在编程中可能还需要完成下面的任务:

def containsOnly(seq, aset):
  """检测序列seq中的成员是否只包含aset中的成员"""
  for c in seq:
    if c not in aset: return False
  return True

containsOnly是与containsAny相反的函数。其他类似的需要遍历所有成员的任务,我们可以使用内置对象set来完成:

def containsAll(seq, aset):
  """检测序列seq中是否包含aset中的全部成员"""
  return not set(aset).difference(seq)

对于集合a, a.difference(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字符串)时,你可能可以尝试使用针对字符串的处理方法,就是使用字符串的 string.translatestring.maketrans 函数,(该内容将在 1.10章节 中作详细介绍) 比如:

import string
notrans = string.maketrans('','')       # 创建翻译对照表
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与字符串不同,它只需要提供提供一个dict对象作为translate的参数,而非字符串使用是提供的两个参数。

参见

菜谱1.10;在Python库函数参考手册以及Python in a Nutshell中关于string和Unicode对象的translate方法,以及string的maketrans方法的介绍,以及对built-in set,sets和itertools模块,以及特殊方法__contains__的介绍。

2 Responses to “Python食谱-1.8.检查字符串中是否包含一个集合中的字符”

  1. [...] s == s.capitalize( ) and containsAny(s, string.letters) 在以上代码中我们用到了在 Python食谱1.8 中所介绍的方法以确保在字符串为空或不包含字母时返回'False'。正如 [...]

Leave a Response