18.3. 优化正则表达式
Description: Python from novice to pro
Keywords: Python, Dive Into Python, tutorial, object-oriented, programming, documentation, book, free

# 深入 Python :Dive Into Python 中文版 Python 从新手到专家 [Dip_5.4b_CPyUG_Release] | Find:

## 18.3. 优化正则表达式

Soundex 函数的第一件事是检查输入是否是一个空字符串。怎样做是最好的方法？

如果你回答 “正则表达式”，坐在角落里反省你糟糕的直觉。正则表达式几乎永远不是最好的答案，而且应该被尽可能避开。这不仅仅是基于性能考虑，而是因为调试和维护都很困难，当然性能也是个原因。

这是 `soundex/stage1/soundex1a.py` 检查 `source` 是否全部由字母构成的一段代码，至少是一个字母 (而不是空字符串)：

        allChars = string.uppercase + string.lowercase
        if not re.search('^[%s]+$' % allChars, source):
            return "0000"

`soundex1a.py` 表现如何？为了方便，`__main__` 部分包含了一段代码：调用 `timeit` 模块，为三个不同名字分别建立测试，依次测试，并显示每个测试的最短耗时：

    if __name__ == '__main__':
        from timeit import Timer
        names = ('Woo', 'Pilgrim', 'Flingjingwaller')
        for name in names:
            statement = "soundex('%s')" % name
            t = Timer(statement, "from __main__ import soundex")
            print name.ljust(15), soundex(name), min(t.repeat())

那么，应用正则表达式的 `soundex1a.py` 表现如何呢？

    C:\samples\soundex\stage1>python soundex1a.py
    Woo             W000 19.3356647283
    Pilgrim         P426 24.0772053431
    Flingjingwaller F452 35.0463220884

正如你预料，名字越长，算法耗时就越长。有几个工作可以令我们减小这个差距 (使函数对于长输入花费较短的相对时间) 但是算法的本质决定它不可能每次运行时间都相同。

另一点应铭记于心的是，我们测试的是有代表性的名字样本。`Woo` 是个被缩短到单字符并补零的小样本；`Pilgrim` 是个夹带着特别字符和忽略字符的平均长度的正常样本；`Flingjingwaller` 是一个包含连续重复字符并且特别长的样本。其它的测试可能同样有帮助，但它们已经很好地代表了不同的样本范围。

那么那个正则表达式如何呢？嗯，缺乏效率。因为这个表达式测试不止一个范围的字符 (`A-Z` 的大写范围和 `a-z` 的小写字母范围)，我们可以使用一个正则表达式的缩写语法。这便是 `soundex/stage1/soundex1b.py`:

        if not re.search('^[A-Za-z]+$', source):
            return "0000"

`timeit` 显示 `soundex1b.py` 比 `soundex1a.py` 稍微快一些，但是没什么令人激动的变化：

    C:\samples\soundex\stage1>python soundex1b.py
    Woo             W000 17.1361133887
    Pilgrim         P426 21.8201693232
    Flingjingwaller F452 32.7262294509

在 第 15.3 节 “重构” 中我们看到正则表达式可以被编译并在重用时以更快速度获得结果。因为这个正则表达式在函数中每次被调用时都不变化，我们可以编译它一次并使用被编译的版本。这便是 `soundex/stage1/soundex1c.py`：

    isOnlyChars = re.compile('^[A-Za-z]+$').search
    def soundex(source):
        if not isOnlyChars(source):
            return "0000"

`soundex1c.py` 中使用被编译的正则表达式产生了显著的提速：

    C:\samples\soundex\stage1>python soundex1c.py
    Woo             W000 14.5348347346
    Pilgrim         P426 19.2784703084
    Flingjingwaller F452 30.0893873383

但是这样的优化是正路吗？这里的逻辑很简单：输入 `source` 应该是非空，并且需要完全由字母构成。如果编写一个循环查看每个字符并且抛弃正则表达式，是否会更快些？

这便是 `soundex/stage1/soundex1d.py`：

        if not source:
            return "0000"
        for c in source:
            if not ('A' <= c <= 'Z') and not ('a' <= c <= 'z'):
                return "0000"

这个技术在 `soundex1d.py` 中恰好不及 编译后的正则表达式快 (尽管比使用未编译的正则表达式快[14])：

    C:\samples\soundex\stage1>python soundex1d.py
    Woo             W000 15.4065058548
    Pilgrim         P426 22.2753567842
    Flingjingwaller F452 37.5845122774

为什么 `soundex1d.py` 没能更快？答案来自 Python 的编译本质。正则表达式引擎以 C 语言编写，被编译后则能本能地在你的计算机上运行。另一方面，循环是以 Python 编写，要通过 Python 解释器。尽管循环相对简单，但没能简单到补偿花在代码解释上的时间。正则表达式永远不是正确答案……但例外还是存在的。

恰巧 Python 提供了一个晦涩的字符串方法。你有理由不了解它，因为本书未曾提到它。这个方法便是 `isalpha()`，它检查一个字符串是否只包含字母。

这便是 `soundex/stage1/soundex1e.py`：

        if (not source) and (not source.isalpha()):
            return "0000"

在 `soundex1e.py` 中应用这个特殊方法我们能得到多少好处? 很多。

    C:\samples\soundex\stage1>python soundex1e.py
    Woo             W000 13.5069504644
    Pilgrim         P426 18.2199394057
    Flingjingwaller F452 28.9975225902

### 例 18.3. 目前为止最好的结果：`soundex/stage1/soundex1e.py`

    import string, re

    charToSoundex = {"A": "9",
                     "B": "1",
                     "C": "2",
                     "D": "3",
                     "E": "9",
                     "F": "1",
                     "G": "2",
                     "H": "9",
                     "I": "9",
                     "J": "2",
                     "K": "2",
                     "L": "4",
                     "M": "5",
                     "N": "5",
                     "O": "9",
                     "P": "1",
                     "Q": "2",
                     "R": "6",
                     "S": "2",
                     "T": "3",
                     "U": "9",
                     "V": "1",
                     "W": "9",
                     "X": "2",
                     "Y": "9",
                     "Z": "2"}

    def soundex(source):
        if (not source) and (not source.isalpha()):
            return "0000"
        source = source[0].upper() + source[1:]
        digits = source[0]
        for s in source[1:]:
            s = s.upper()
            digits += charToSoundex[s]
        digits2 = digits[0]
        for d in digits[1:]:
            if digits2[-1] != d:
                digits2 += d
        digits3 = re.sub('9', '', digits2)
        while len(digits3) < 4:
            digits3 += "0"
        return digits3[:4]

    if __name__ == '__main__':
        from timeit import Timer
        names = ('Woo', 'Pilgrim', 'Flingjingwaller')
        for name in names:
            statement = "soundex('%s')" % name
            t = Timer(statement, "from __main__ import soundex")
            print name.ljust(15), soundex(name), min(t.repeat())

### Footnotes

[14] 注意 `soundex1d.py` 在后两个测试点上都比 `soundex1b.py` 慢，这点与作者所说的矛盾。本章另还有多处出现了正文与测试结果矛盾的地方，每个地方都会用译注加以说明。这个 bug 将在下个版本中得到修正。――译注