17.5. plural.py,阶段 4

让我们将代码中的重复部分提取出来,以便更容易定义新的规则。

例 17.9. plural4.py


import re

def buildMatchAndApplyFunctions((pattern, search, replace)):  
    matchFunction = lambda word: re.search(pattern, word)      1
    applyFunction = lambda word: re.sub(search, replace, word) 2
    return (matchFunction, applyFunction)                      3
1 buildMatchAndApplyFunctions 是一个动态构建其他函数的函数。它接受 patternsearchreplace(实际上它接受一个元组,稍后会详细介绍),您可以使用 lambda 语法构建匹配函数,该函数接受一个参数 (word) 并使用传递给 buildMatchAndApplyFunctions 函数的 pattern 以及传递给您正在构建的匹配函数的 word 调用 re.search。哇哦。
2 构建应用函数的方式相同。应用函数是一个接受一个参数的函数,并使用传递给 buildMatchAndApplyFunctions 函数的 searchreplace 参数以及传递给您正在构建的应用函数的 word 调用 re.sub。这种在动态函数中使用外部参数值的技术称为闭包。您实际上是在构建的应用函数中定义常量:它接受一个参数 (word),但随后它会作用于该参数以及在定义应用函数时设置的另外两个值 (searchreplace)。
3 最后,buildMatchAndApplyFunctions 函数返回一个包含两个值的元组:您刚刚创建的两个函数。您在这些函数中定义的常量(matchFunction 中的 pattern 以及 applyFunction 中的 searchreplace)会保留在这些函数中,即使您从 buildMatchAndApplyFunctions 返回后也是如此。这真是太酷了。

如果这令人难以置信地困惑(应该是这样,这很奇怪),那么当您看到如何使用它时,它可能会变得更加清晰。

例 17.10. plural4.py

patterns = \
  (
    ('[sxz]$', '$', 'es'),
    ('[^aeioudgkprt]h$', '$', 'es'),
    ('(qu|[^aeiou])y$', 'y$', 'ies'),
    ('$', '$', 's')
  )                                                 1
rules = map(buildMatchAndApplyFunctions, patterns)  2
1 我们的复数规则现在定义为一系列字符串(而不是函数)。第一个字符串是您将在 re.search 中使用的正则表达式,用于查看此规则是否匹配;第二个和第三个字符串是您将在 re.sub 中使用的搜索和替换表达式,用于实际应用规则将名词转换为其复数形式。
2 这行很神奇。它获取 patterns 中的字符串列表,并将它们转换为函数列表。怎么做?通过将字符串映射到 buildMatchAndApplyFunctions 函数,该函数恰好接受三个字符串作为参数并返回一个包含两个函数的元组。这意味着 rules 最终与前面的示例完全相同:一个元组列表,其中每个元组都是一对函数,其中第一个函数是调用 re.search 的匹配函数,第二个函数是调用 re.sub 的应用函数。

我发誓我没有编造:rules 最终得到的函数列表与前面的示例完全相同。展开 rules 定义,您将得到以下内容

例 17.11. 展开规则定义

rules = \
  (
    (
     lambda word: re.search('[sxz]$', word),
     lambda word: re.sub('$', 'es', word)
    ),
    (
     lambda word: re.search('[^aeioudgkprt]h$', word),
     lambda word: re.sub('$', 'es', word)
    ),
    (
     lambda word: re.search('[^aeiou]y$', word),
     lambda word: re.sub('y$', 'ies', word)
    ),
    (
     lambda word: re.search('$', word),
     lambda word: re.sub('$', 's', word)
    )
   )                                          

例 17.12. plural4.py,完成


def plural(noun):                                  
    for matchesRule, applyRule in rules:            1
        if matchesRule(noun):                      
            return applyRule(noun)                 
1 由于 rules 列表与前面的示例相同,因此 plural 函数没有改变也就不足为奇了。请记住,它是完全通用的;它接受一个规则函数列表并按顺序调用它们。它不关心规则是如何定义的。在阶段 2 中,它们被定义为单独的命名函数。在阶段 3 中,它们被定义为匿名 lambda 函数。现在在阶段 4 中,它们是通过将 buildMatchAndApplyFunctions 函数映射到原始字符串列表来动态构建的。没关系;plural 函数的工作方式仍然相同。

如果这还不够令人兴奋,我必须承认在 buildMatchAndApplyFunctions 的定义中有一个我跳过的微妙之处。让我们回去再看看。

例 17.13. 再看 buildMatchAndApplyFunctions


def buildMatchAndApplyFunctions((pattern, search, replace)):   1
1 注意到双括号了吗?此函数实际上不接受三个参数;它实际上接受一个参数,一个包含三个元素的元组。但是当函数被调用时,元组会被展开,并且元组的三个元素分别被分配给不同的变量:patternsearchreplace。困惑了吗?让我们来看看它的实际效果。

例 17.14. 在调用函数时展开元组

>>> def foo((a, b, c)):
...     print c
...     print b
...     print a
>>> parameters = ('apple', 'bear', 'catnap')
>>> foo(parameters) 1
catnap
bear
apple
1 调用函数 foo 的正确方法是使用一个包含三个元素的元组。当函数被调用时,这些元素会被分配给 foo 中不同的局部变量。

现在让我们回过头来看看为什么需要这种自动元组扩展技巧。patterns 是一个元组列表,每个元组都有三个元素。当您调用 map(buildMatchAndApplyFunctions, patterns) 时,这意味着 buildMatchAndApplyFunctions不会 使用三个参数调用。使用 map 将单个列表映射到函数总是使用单个参数调用函数:列表的每个元素。在 patterns 的情况下,列表的每个元素都是一个元组,因此 buildMatchAndApplyFunctions 总是使用元组调用,并且您在 buildMatchAndApplyFunctions 的定义中使用自动元组扩展技巧将该元组的元素分配给您可以使用的命名变量。