您当前位置: 首页 ‣ 深入 Python 3 ‣
难度等级: ♦♦♦♢♢
❝ 我的拼写很糟糕。它拼写不错,但很糟糕,字母总是出现在错误的位置。 ❞
— 小熊维尼
我从小就受身为图书管理员和英语专业的父亲的影响,我一直对语言着迷。不是编程语言。当然,编程语言也很有趣,但自然语言更令人着迷。比如英语。英语是一种精神分裂的语言,它借鉴了德语、法语、西班牙语和拉丁语(仅举几例)的词汇。实际上,“借鉴”这个词用错了;“掠夺”更准确。或者也许是“同化”——就像博格人一样。是的,我喜欢这个词。
我们是博格人。你的语言学和词源学特征将被添加到我们自己的特征中。抵抗是徒劳的。
在本章中,您将学习关于复数名词的知识。另外,还有返回其他函数的函数、高级正则表达式和生成器。但首先,让我们来谈谈如何制作复数名词。(如果您还没有阅读 关于正则表达式的章节,现在是个好时机。本章假设您理解正则表达式的基本知识,并且很快就会深入到更高级的应用中。)
如果您在英语国家长大或在正规学校学习英语,您可能熟悉基本规则
(我知道,有很多例外。Man 变成 men,woman 变成 women,但 human 变成 humans。Mouse 变成 mice,louse 变成 lice,但 house 变成 houses。Knife 变成 knives,wife 变成 wives,但 lowlife 变成 lowlifes。更别提那些本身就是复数的词,比如 sheep、deer 和 haiku。)
当然,其他语言完全不同。
让我们设计一个 Python 库,它可以自动将英语名词变为复数。我们先从这四个规则开始,但请记住,您必然需要添加更多规则。
⁂
所以您正在查看单词,至少在英语中,这意味着您正在查看字符的字符串。您有一些规则说您需要查找不同字符组合,然后对它们进行不同的处理。这听起来像是正则表达式的任务!
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'
[sxz]
表示“s
或 x
或 z
”,但只能匹配一个。$
应该很熟悉;它匹配字符串的结尾。组合起来,这个正则表达式测试 noun 是否以 s
、x
或 z
结尾。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
。现在,让我们回到 plural()
函数……
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'
es
替换字符串的结尾(由 $
匹配)。换句话说,就是将 es
添加到字符串中。您可以使用字符串连接来完成相同的操作,例如 noun + 'es'
,但我选择对每个规则使用正则表达式,原因将在本章后面说明。^
作为方括号内的第一个字符具有特殊含义:否定。[^abc]
表示“任何除了 a
、b
或 c
之外的单个字符”。因此 [^aeioudgkprt]
表示除了 a
、e
、i
、o
、u
、d
、g
、k
、p
、r
或 t
之外的任何字符。然后,该字符需要后跟 h
,最后是字符串的结尾。您正在查找以 H 结尾的单词,其中 H 可以被听到。a
、e
、i
、o
或 u
。您正在查找以 Y 结尾且发音像 I 的单词。让我们更详细地了解否定正则表达式。
>>> 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
结尾。>>> 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
。y
之前的字符。然后在替换字符串中,您使用了一种新的语法 \1
,它表示“嘿,您记住的第一个组?把它放在这里”。在这种情况下,您记住 y
之前的 c
;当您进行替换时,您用 c
替换 c
,用 ies
替换 y
。(如果您有多个记忆组,可以使用 \2
和 \3
等等。)正则表达式替换非常强大,\1
语法使它们更加强大。但是将整个操作组合成一个正则表达式也更难阅读,并且它不直接映射到您最初描述的复数化规则。您最初制定了类似“如果单词以 S、X 或 Z 结尾,则添加 ES”的规则。如果您查看此函数,您有两行代码说“如果单词以 S、X 或 Z 结尾,则添加 ES”。这已经非常直接了。
⁂
现在您将添加一层抽象。您最初定义了一组规则:如果满足此条件,则执行此操作,否则转到下一条规则。让我们暂时使程序的一部分变得复杂,以便您可以简化另一部分。
import re
def match_sxz(noun):
return re.search('[sxz]$', noun)
def apply_sxz(noun):
return re.sub('$', 'es', noun)
def match_h(noun):
return re.search('[^aeioudgkprt]h$', noun)
def apply_h(noun):
return re.sub('$', 'es', noun)
def match_y(noun): ①
return re.search('[^aeiou]y$', noun)
def apply_y(noun): ②
return re.sub('y$', 'ies', noun)
def match_default(noun):
return True
def apply_default(noun):
return noun + 's'
rules = ((match_sxz, apply_sxz), ③
(match_h, apply_h),
(match_y, apply_y),
(match_default, apply_default)
)
def plural(noun):
for matches_rule, apply_rule in rules: ④
if matches_rule(noun):
return apply_rule(noun)
re.search()
函数的结果。re.sub()
函数以应用相应的复数化规则。plural()
) 来包含多个规则,而是使用 rules
数据结构,它是一个函数对序列。plural()
函数可以简化为几行代码。使用 for
循环,您可以一次性从 rules 结构中取出匹配规则和应用规则(一个匹配,一个应用)。在 for
循环的第一次迭代中,matches_rule 将获得 match_sxz
,而 apply_rule 将获得 apply_sxz
。在第二次迭代(假设您到达这一步)中,matches_rule 将被赋值为 match_h
,而 apply_rule 将被赋值为 apply_h
。该函数保证最终会返回某个值,因为最终的匹配规则 (match_default
) 只返回 True
,这意味着对应的应用规则 (apply_default
) 将始终被应用。这种技术之所以有效,是因为 Python 中的一切都是对象,包括函数。rules 数据结构包含函数——不是函数的名称,而是实际的函数对象。当它们在 for
循环中被赋值时,matches_rule 和 apply_rule 就会成为可以调用的实际函数。在 for
循环的第一次迭代中,这相当于调用 matches_sxz(noun)
,如果它返回一个匹配项,则调用 apply_sxz(noun)
。
如果这额外的抽象层令人困惑,请尝试展开函数以查看等效关系。整个 for
循环等效于以下代码
def plural(noun):
if match_sxz(noun):
return apply_sxz(noun)
if match_h(noun):
return apply_h(noun)
if match_y(noun):
return apply_y(noun)
if match_default(noun):
return apply_default(noun)
这里的好处是 plural()
函数现在得到了简化。它接受一个规则序列(在其他地方定义),并以通用方式迭代它们。
规则可以定义在任何地方,以任何方式定义。plural()
函数并不关心。
现在,添加这层抽象值得吗?嗯,还没。让我们考虑一下向函数添加新规则需要做什么。在第一个示例中,这将需要在 plural()
函数中添加一个 if
语句。在第二个示例中,这将需要添加两个函数 match_foo()
和 apply_foo()
,然后更新 rules 序列以指定新的匹配函数和应用函数应该相对于其他规则按什么顺序调用。
但这仅仅是通往下一节的垫脚石。让我们继续……
⁂
为每个匹配规则和应用规则定义单独命名的函数实际上没有必要。您永远不会直接调用它们;您将它们添加到 rules 序列中,并通过该序列调用它们。此外,每个函数都遵循两种模式之一。所有匹配函数都调用 re.search()
,所有应用函数都调用 re.sub()
。让我们将模式提取出来,以便更容易地定义新规则。
import re
def build_match_and_apply_functions(pattern, search, replace):
def matches_rule(word): ①
return re.search(pattern, word)
def apply_rule(word): ②
return re.sub(search, replace, word)
return (matches_rule, apply_rule) ③
build_match_and_apply_functions()
是一个动态构建其他函数的函数。它接收 pattern、search 和 replace,然后定义一个 matches_rule()
函数,该函数使用传递给 build_match_and_apply_functions()
函数的 pattern 和传递给您正在构建的 matches_rule()
函数的 word 调用 re.search()
。哇。build_match_and_apply_functions()
函数的 search 和 replace 参数,以及传递给您正在构建的 apply_rule()
函数的 word 调用 re.sub()
。这种在动态函数中使用外部参数值的技术称为闭包。您本质上是在构建的 apply 函数中定义常量:它接受一个参数 (word),但它会根据该参数加上另外两个值 (search 和 replace) 进行操作,这两个值是在您定义 apply 函数时设置的。build_match_and_apply_functions()
函数返回一个包含两个值的元组:您刚刚创建的两个函数。您在这些函数中定义的常量 (pattern 在 matches_rule()
函数中,search 和 replace 在 apply_rule()
函数中) 会一直保留在这些函数中,即使您从 build_match_and_apply_functions()
中返回也是如此。这太酷了。如果这非常令人困惑(确实应该如此,这很奇怪),那么当您看到如何使用它时,它可能会变得更清楚。
patterns = \ ①
(
('[sxz]$', '$', 'es'),
('[^aeioudgkprt]h$', '$', 'es'),
('(qu|[^aeiou])y$', 'y$', 'ies'),
('$', '$', 's') ②
)
rules = [build_match_and_apply_functions(pattern, search, replace) ③
for (pattern, search, replace) in patterns]
re.search()
中使用的正则表达式模式,以查看该规则是否匹配。每个组中的第二个和第三个字符串是您将在 re.sub()
中使用的搜索和替换表达式,以实际应用规则将名词转换为其复数形式。match_default()
函数只是返回 True
,这意味着如果没有任何更具体的规则匹配,代码将只在给定单词的末尾添加一个 s
。这个例子做了一些在功能上等效的事情。最后的正则表达式询问该单词是否有一个结尾 ($
与字符串的结尾匹配)。当然,每个字符串都有一个结尾,即使是一个空字符串也是如此,所以这个表达式总是匹配。因此,它与总是返回 True
的 match_default()
函数具有相同的作用:它确保如果没有任何更具体的规则匹配,代码会在给定单词的末尾添加一个 s
。build_match_and_apply_functions()
函数。也就是说,它接受每个字符串三元组,并使用这三个字符串作为参数调用 build_match_and_apply_functions()
函数。build_match_and_apply_functions()
函数返回一个包含两个函数的元组。这意味着 rules 最终在功能上等效于前面的示例:一个函数列表,其中每个元组都是一对函数。第一个函数是调用 re.search()
的匹配函数,第二个函数是调用 re.sub()
的应用函数。这个版本的脚本的最后部分是主入口点,即 plural()
函数。
def plural(noun):
for matches_rule, apply_rule in rules: ①
if matches_rule(noun):
return apply_rule(noun)
plural()
函数完全没有改变也就不足为奇了。它完全是通用的;它接受一个规则函数列表,并按顺序调用它们。它不关心规则是如何定义的。在前面的示例中,它们是作为独立命名的函数定义的。现在,它们是通过将 build_match_and_apply_functions()
函数的输出映射到一个原始字符串列表来动态构建的。这并不重要;plural()
函数仍然以相同的方式工作。⁂
您已经提取了所有重复的代码,并添加了足够的抽象,以便复数化规则定义在一个字符串列表中。下一个合乎逻辑的步骤是将这些字符串放入一个单独的文件中,以便它们可以与使用它们的代码分开维护。
首先,让我们创建一个包含您想要规则的文本文件。没有花哨的数据结构,只有三列以空格分隔的字符串。让我们将其命名为 plural4-rules.txt
。
[sxz]$ $ es
[^aeioudgkprt]h$ $ es
[^aeiou]y$ y$ ies
$ $ s
现在让我们看看如何使用这个规则文件。
import re
def build_match_and_apply_functions(pattern, search, replace): ①
def matches_rule(word):
return re.search(pattern, word)
def apply_rule(word):
return re.sub(search, replace, word)
return (matches_rule, apply_rule)
rules = []
with open('plural4-rules.txt', encoding='utf-8') as pattern_file: ②
for line in pattern_file: ③
pattern, search, replace = line.split(None, 3) ④
rules.append(build_match_and_apply_functions( ⑤
pattern, search, replace))
build_match_and_apply_functions()
函数没有改变。您仍然使用闭包来动态构建两个使用外部函数中定义的变量的函数。open()
函数打开一个文件并返回一个文件对象。在本例中,我们打开的文件包含用于将名词复数化的模式字符串。with
语句创建了一个名为上下文的东西:当 with
块结束时,Python 会自动关闭文件,即使在 with
块内引发了异常。您将在 文件 章节中了解有关 with
块和文件对象的更多信息。for line in <fileobject>
惯用法一次读取打开文件中的数据,并将其分配给 line 变量。您将在 文件 章节中了解有关从文件读取的更多信息。split()
字符串方法。split()
方法的第一个参数是 None
,这意味着“以任何空格 (制表符或空格,没有区别) 分隔”。第二个参数是 3
,这意味着“以空格分隔 3 次,然后保留该行的剩余部分”。像 [sxz]$ $ es
这样的行将被分解成列表 ['[sxz]$', '$', 'es']
,这意味着 pattern 将获得 '[sxz]$'
,search 将获得 '$'
,而 replace 将获得 'es'
。在一行小小的代码中包含了如此强大的功能。pattern
、search
和 replace
传递给 build_match_and_apply_functions()
函数,该函数返回一个包含函数的元组。您将这个元组追加到 rules 列表中,而 rules 最终存储了 plural()
函数期望的匹配和应用函数列表。这里的改进在于您已将复数化规则完全分离到一个外部文件中,因此它可以与使用它的代码分开维护。代码是代码,数据是数据,生活很美好。
⁂
如果有一个通用的 plural()
函数可以解析规则文件,那岂不是很好?获取规则,检查匹配项,应用适当的转换,转到下一条规则。这就是 plural()
函数需要做的,也是 plural()
函数应该做的。
def rules(rules_filename):
with open(rules_filename, encoding='utf-8') as pattern_file:
for line in pattern_file:
pattern, search, replace = line.split(None, 3)
yield build_match_and_apply_functions(pattern, search, replace)
def plural(noun, rules_filename='plural5-rules.txt'):
for matches_rule, apply_rule in rules(rules_filename):
if matches_rule(noun):
return apply_rule(noun)
raise ValueError('no matching rule for {0}'.format(noun))
这到底是怎么做到的?让我们先看看一个交互式示例。
>>> def make_counter(x): ... print('entering make_counter') ... while True: ... yield x ① ... print('incrementing x') ... x = x + 1 ... >>> counter = make_counter(2) ② >>> counter ③ <generator object at 0x001C9C10> >>> next(counter) ④ entering make_counter 2 >>> next(counter) ⑤ incrementing x 3 >>> next(counter) ⑥ incrementing x 4
make_counter
中存在 yield
关键字意味着这不是一个普通函数。它是一种特殊的函数,可以一次生成一个值。您可以将其视为可恢复函数。调用它将返回一个生成器,该生成器可用于生成 x 的连续值。make_counter
生成器的实例,只需像任何其他函数一样调用它。请注意,这实际上并没有执行函数代码。您可以通过查看 make_counter()
函数的第一行调用 print()
,但还没有打印任何内容来判断这一点。make_counter()
函数返回一个生成器对象。next()
函数接受一个生成器对象并返回其下一个值。第一次使用 counter 生成器调用 next()
时,它会执行 make_counter()
中的代码,直到第一个 yield
语句,然后返回所产生的值。在本例中,这将是 2
,因为您最初是通过调用 make_counter(2)
来创建生成器的。next()
会从它停止的地方继续执行,直到遇到下一个 yield
语句。所有变量、本地状态、&c. 都会在 yield
上保存,并在 next()
上恢复。要执行的下一行代码调用 print()
,它会打印 incrementing x。之后,语句 x = x + 1
。然后它再次循环遍历 while
循环,它遇到的第一件事是语句 yield x
,该语句保存所有内容的状态并返回 x 的当前值(现在是 3
)。next(counter)
时,您会再次执行所有相同的事情,但这次 x 现在是 4
。由于 make_counter
设置了一个无限循环,因此理论上您可以永远执行此操作,它只会不断递增 x 并输出值。但让我们看看生成器的更有效用途。
def fib(max):
a, b = 0, 1 ①
while a < max:
yield a ②
a, b = b, a + b ③
1
开始,最初缓慢增加,然后越来越快。要启动该序列,您需要两个变量:a 从 0 开始,b 从 1
开始。a + b
) 并将其分配给 b 以备后用。请注意,这是并行发生的;如果 a 是 3
且 b 是 5
,则 a, b = b, a + b
将将 a 设置为 5
(b 的前一个值)并将 b 设置为 8
(a 和 b 的前一个值的总和)。所以您有一个函数可以输出连续的斐波那契数。当然,您可以使用递归来完成,但这种方式更易于阅读。此外,它与 for
循环配合使用效果很好。
>>> from fibonacci import fib >>> for n in fib(1000): ① ... print(n, end=' ') ② 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 >>> list(fib(1000)) ③ [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987]
for
循环中使用像 fib()
这样的生成器。for
循环会自动调用 next()
函数以从 fib()
生成器获取值,并将它们分配给 for
循环索引变量 (n)。for
循环时,n 都会从 fib()
中的 yield
语句获取一个新值,您只需将其打印出来即可。一旦 fib()
用完数字 (a 变得大于 max,在本例中为 1000
),for
循环就会优雅地退出。list()
函数,它将遍历整个生成器(就像前面的示例中的 for
循环一样)并返回所有值的列表。让我们回到 plural5.py
并看看这个版本的 plural()
函数是如何工作的。
def rules(rules_filename):
with open(rules_filename, encoding='utf-8') as pattern_file:
for line in pattern_file:
pattern, search, replace = line.split(None, 3) ①
yield build_match_and_apply_functions(pattern, search, replace) ②
def plural(noun, rules_filename='plural5-rules.txt'):
for matches_rule, apply_rule in rules(rules_filename): ③
if matches_rule(noun):
return apply_rule(noun)
raise ValueError('no matching rule for {0}'.format(noun))
line.split(None, 3)
获取三个“列”并将它们分配给三个局部变量。build_match_and_apply_functions()
动态构建,它与前面的示例完全相同。换句话说,rules()
是一个按需输出匹配和应用函数的生成器。rules()
是一个生成器,您可以直接在 for
循环中使用它。第一次循环时,您将调用 rules()
函数,该函数将打开模式文件,读取第一行,根据该行中的模式动态构建匹配函数和应用函数,并生成动态构建的函数。第二次循环时,您将从 rules()
中您上次离开的地方继续(它在 for line in pattern_file
循环的中间)。它将做的第一件事是读取文件的下一行(该文件仍然处于打开状态),根据文件该行上的模式动态构建另一个匹配和应用函数,并生成这两个函数。与阶段 4 相比,您获得了什么?启动时间。在阶段 4 中,当您导入 plural4
模块时,它会读取整个模式文件并构建所有可能规则的列表,甚至在您开始调用 plural()
函数之前。使用生成器,您可以延迟执行所有操作:您读取第一条规则并创建函数并尝试它们,如果成功,您将永远不会读取文件的其余部分或创建任何其他函数。
您损失了什么?性能!每次调用 plural()
函数时,rules()
生成器都会从头开始,这意味着重新打开模式文件并从头开始逐行读取。
如果您能两全其美:最小的启动成本(在 import
时不执行任何代码),以及最佳性能(不要反复构建相同的函数)。哦,您仍然希望将规则保存在单独的文件中(因为代码是代码,数据是数据),只要您不必读取同一行两次即可。
为此,您需要构建自己的迭代器。但在您构建 它 之前,您需要学习 Python 类。
⁂
© 2001–11 Mark Pilgrim