1.18 一次完成多个替换
任务
你想对字符串的某些子串进行替换。
解决方案
正则表达式虽然不易读懂,但有时它的确是最快的方法。re对象(标准库中的re模块)提供的强大sub方法,非常利于进行高效的正则表达式匹配替换。下面给出一个函数,该函数返回一个输入字符串的拷贝,该拷贝中的所有能够在指定字典中找到的子串都被替换为字典中的对应值:
import re
def multiple_replace(text, adict):
rx = re.compile('|'.join(map(re.escape, adict)))
def one_xlat(match):
return adict[match.group(0)]
return rx.sub(one_xlat, text)
讨论
本节展示了怎样使用Python的标准模块re来一次完成多个子串的替换。假设你有个基于字典的字符串的映射关系。字典的key就是你想要替换的子串,而字典中key的对应值则正是被用来做替代物的字符串。也可以针对字典的键值对应关系,调用字符串方法replace来完成替换,它将多次处理和创建原文本的复制,但逻辑却很清晰,速度也不错。不过re.sub的回调函数机制可以让处理方式变得更加简单。
首先,我们根据想要匹配的key创建一个正则表达式。这个正则表达式形式为a1|a2|...|aN,由N个需要被替换的字符串组成,并被竖线隔开,创建的方法也很简单,如代码所示,一行代码完成。然后,我们不直接给re.sub传递用于替换的字符串,而是传入一个回调函数参数。这样,每当遇到一次匹配,re.sub就会调用该回调函数,并将re.MatchObject的实例作为唯一参数传递给该回调函数,并期望着该回调函数返回作为替换物的字符串。在本例中,回调函数在字典中查找匹配的文本,并返回了对应值。
本节展示的函数multiple_replace,每次被调用时都会重新计算正则表达式并重新定义one_xlat辅助函数。但你经常只需要使用同一个固定不变的翻译表来完成很多文本的替换,这种情况下也许会希望只做一次准备工作。出于这种需求,也许会使用下面的基于闭包的方式:
import re
def make_xlat(args, *kwds):
adict = dict(args, *kwds)
rx = re.compile('|'.join(map(re.escape, adict)))
def one_xlat(match):
return adict[match.group(0)]
def xlat(text):
return rx.sub(one_xlat, text)
return xlat
可以给make_xlat函数传递一个字典参数,或者其他的可以传递给内建的dict用于创建一个字典的参数组合;make_xlat返回一个xlat闭包,它只需要一个字符串参数text,并返回text的一个拷贝,该拷贝是根据字典给出的翻译表完成了替换之后的结果。
下面给出应用此函数的例子。通常我们可以把这个片段中的示例代码作为本节给出的代码源文件的一部分,这段代码受到前面的Python语句的保护不会被执行,除非这个模块被作为主脚本被执行:
if _ _name_ _ == "_ _main_ _":
text = "Larry Wall is the creator of Perl"
adict = {
"Larry Wall" : "Guido van Rossum",
"creator" : "Benevolent Dictator for Life",
"Perl" : "Python",
}
print multiple_replace(text, adict)
translate = make_xlat(adict)
print translate(text)
本节中的替换任务常常是基于单词的替换任务,而不是基于任意一个子字符串。通过特殊的r’\b’序列,正则表达式可以很好地找出单词的开始和结束位置。我们可以修改multiple_replace和make_xlat中创建和分配正则表达式rx的部分,从而完成一些自定义任务:
rx = re.compile(r'\b%s\b' % r'\b|\b'.join(map(re.escape, adict)))
其余的代码和本节前面给出的一样。但是,这种代码相似性可不是好事:那意味着我们需要很多相似的版本,每个创建正则表达式的部分都有点不同,我们可能会需要做大量的复制粘贴工作,这是代码复用中最糟糕的情况,另外在未来的维护上也增加了麻烦。
编写好代码的一个关键规则是:“一次,只做一次!”当我们注意到代码重复的时候,应该能够很快嗅到“不妙”的气味,并对原来的代码进行重构以提高复用性。因此,为了便于定制,我们更需要的是一个类,而不是函数或者闭包。下面给出个例子,我们实现了一个类,功能近似于make_xlat,但却能够通过子类化和重载进行定制:
class make_xlat:
def _ _init_ _ (self, args, *kwds):
self.adict = dict(args, *kwds)
self.rx = self.make_rx( )
def make_rx(self):
return re.compile('|'.join(map(re.escape, self.adict)))
def one_xlat(self, match):
return self.adict[match.group(0)]
def _ _call_ _ (self, text):
return self.rx.sub(self.one_xlat, text)
这是对makexlt函数的一个完全的替代:另一方面,我们在这之前展示的代码,由于有if name == ‘ main _’来保护,即使make_xlat由以前的函数变成了类,也不会有什么问题。函数更加简单快速,但是类的优势是可以通过面向对象的方法—子类化或重载某些函数,轻易地实现重新定制。为了对单词进行翻译替换,代码可以这样写:
class make_xlat_by_whole_words(make_xlat):
def make_rx(self):
return re.compile(r'\b%s\b' % r'\b|\b'.join(map(re.escape, self.adict)))
通过简单的子类化和重载来实现定制化,我们避免了对代码的大量的复制和粘贴,这也是有时我们宁可舍弃更加简单的函数或者闭包不用,而使用面向对象结构的原因。仅仅把相关的功能打包成一个类并不能自动实现需要的定制。要实现高度的可定制性,在把功能划分成独立的类方法时必须具有一定的前瞻性。幸好,你不用逼自己第一次就把代码写得很完美;如果代码中没有能够符合任务需求的内部结构时(在这个例子中,我们通过子类化和选择性重载来复用代码),可以而且也应该对代码进行重构,构建出符合需求的结构。当然需要进行一些合适的测试来确保没有把原有的逻辑破坏掉,与此同时,你也完成了对自己的思想内容的重构。访问http://www.refactoring.com 可以获得更多关于重构的艺术和实践的信息。