17.2. plural.py, 第 1 阶段
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:

## 17.2. `plural.py`, 第 1 阶段

你所针对的单词 (至少在英语中) 是字符串和字符。你还需要规则来找出不同的字符 (字母) 组合，并对它们进行不同的操作。这听起来像是正则表达式的工作。

### 例 17.1. `plural1.py`

    import re

    def plural(noun):
        if re.search('[sxz]$', noun):
            return re.sub('$', 'es', noun)
        elif re.search('[^aeioudgkprt]h$', noun):
            return re.sub('$', 'es', noun)
        elif re.search('[^aeiou]y$', noun):
            return re.sub('y$', 'ies', noun)
        else:
            return noun + 's'

| 好啦，这是一个正则表达式，但是它使用了你在 第 7 章 正则表达式 中未曾见过的语法。方括号的意思是 “完全匹配这些字符中的一个”。也就是说，`[sxz]` 意味着 “`s`，或者 `x`，再或者 `z`”，但只是其中的一个。`$` 应该不陌生，它意味着匹配字符串的结尾。也就是说，检查 `noun` 是否以 `s`，`x`，或者 `z` 结尾。
| `re.sub` 函数进行以正则表达式为基础的替换工作。让我们更具体地看看它。

### 例 17.2. `re.sub` 介绍

    >>> import re
    >>> re.search('[abc]', 'Mark')
    <_sre.SRE_Match object at 0x001C1FA8>
    >>> re.sub('[abc]', 'o', 'Mark')
    'Mork'
    >>> re.sub('[abc]', 'o', 'rock')
    'rook'
    >>> re.sub('[abc]', 'o', 'caps')
    'oops'

| `Mark` 包含 `a`，`b`，或者 `c`吗？是的，含有 `a`。
| 好的，现在找出 `a`，`b`，或者 `c` 并以 `o` 取代之。`Mark` 就变成 `Mork` 了。
| 同一方法可以将 `rock` 变成 `rook`。
| 你可能认为它可以将 `caps` 变成 `oaps`，但事实并非如此。`re.sub` 替换所有 的匹配项，并不只是第一个匹配项。因此正则表达式将会把 `caps` 变成 `oops`，因为 `c` 和 `a` 都被转换为 `o`了。

### 例 17.3. 回到 `plural1.py`

    import re

    def plural(noun):
        if re.search('[sxz]$', noun):
            return re.sub('$', 'es', noun)
        elif re.search('[^aeioudgkprt]h$', noun):
            return re.sub('$', 'es', noun)
        elif re.search('[^aeiou]y$', noun):
            return re.sub('y$', 'ies', noun)
        else:
            return noun + 's'

| 回到 `plural` 函数。你在做什么？你在以 `es` 取代字符串的结尾。换句话说，追加 `es` 到字符串。你可以通过字符串拼合做到相同的事，例如 `noun + 'es'`，但是我使用正则表达式做这一切，既是为了保持一致，也是为了本章稍后你会明白的其它原因。
| 仔细看看，这是另一个新的内容。`^` 是方括号里面的第一个字符，这有特别的含义：否定。`[^abc]` 意味着 “ 除 `a`、 `b`、 和 `c` 以外的 任意单字符”。所以，`[^aeioudgkprt]` 意味着除 `a`、 `e`、 `i`、 `o`、 `u`、 `d`、 `g`、 `k`、 `p`、 `r` 和 `t` 以外的任意字符。这个字符之后应该跟着一个 `h`，然后是字符串的结尾。你在寻找的是以发音的 H 结尾的单词。
| 这是一个相似的表达：匹配 Y 前面不是 `a`、 `e`、 `i`、 `o` 和 `u`，并以这个 Y 结尾的单词。你在查找的是以发 I 音的 Y 结尾的单词。

### 例 17.4. 正则表达式中否定的更多应用

    >>> import re
    >>> re.search('[^aeiou]y$', 'vacancy')
    <_sre.SRE_Match object at 0x001C1FA8>
    >>> re.search('[^aeiou]y$', 'boy')
    >>>
    >>> re.search('[^aeiou]y$', 'day')
    >>>
    >>> re.search('[^aeiou]y$', 'pita')
    >>>

| `vacancy` 匹配这个正则表达式，因为它以 `cy` 结尾，并且 `c` 不在 `a`、 `e`、 `i`、 `o` 和 `u` 之列。
| `boy` 不能匹配，因为它以 `oy` 结尾，并且你特别指出 `y` 之前的字符不可以是 `o`。`day` 不能匹配是因为以 `ay` 结尾。
| `pita` 不匹配是因为不以 `y` 结尾。

### 例 17.5. 更多的 `re.sub`

    >>> re.sub('y$', 'ies', 'vacancy')
    'vacancies'
    >>> re.sub('y$', 'ies', 'agency')
    'agencies'
    >>> re.sub('([^aeiou])y$', r'\1ies', 'vacancy')
    'vacancies'

| 正则表达式把 `vacancy` 变为 `vacancies`，把 `agency` 变为 `agencies`，这正是你想要的。注意，将 `boy` 变成 `boies` 是可行的，但是永远不会发生，因为 `re.search` 首先确定是否应该应用 `re.sub`。
| 顺便提一下，可以将两个正则表达式 (一个确定规则适用与否，一个应用规则) 合并在一起成为一个正则表达式。这便是合并后的样子。它的大部分已经很熟悉：你应用的是在 第 7.6 节 “个案研究：解析电话号码” 学过的记忆组 (remembered group) 记住 `y` 之前的字符。然后再替换字符串，你使用一个新的语法 `\1`，这意味着：“嘿！记得前面的第一个组吗？把它放这儿”。就此而言，记住了 `y` 之前的 `c` ，然后你做替换工作，你将 `c` 替换到 `c` 的位置，并将 `ies` 替换到 `y` 的位置。(如果你有不止一个组则可以使用 `\2` 或者 `\3` 等等。)

正则表达式替换非常强大，并且 `\1` 语法使之更加强大。但是将整个操作放在一个正则表达式中仍然晦涩难懂，也不能与前面描述的复数规则直接呼应。你原来列出的规则，比如 “如果单词以 S，X 或者 Z 结尾，结尾追加 ES”。如果你在函数中看到两行代码描述 “如果单词以 S，X 或者 Z 结尾，结尾追加 ES”，更加直观些。