This repository has been archived by the owner on Dec 10, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 12
/
generators.html
306 lines (258 loc) · 38.4 KB
/
generators.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
<!DOCTYPE html>
<meta charset=utf-8>
<title>闭合与生成器 -- 深入 Python 3</title><!--[if IE]><script src=j/html5.js></script><![endif]-->
<link rel=stylesheet href="dip3.css">
<style>
body{counter-reset:h1 6}
</style>
<link rel=stylesheet media='only screen and (max-device-width: 480px)' href="http://woodpecker.org.cn/diveintopython3/mobile.css">
<link rel=stylesheet media=print href="http://woodpecker.org.cn/diveintopython3/print.css">
<meta name=viewport content='initial-scale=1.0'>
<form action=http://www.google.com/cse><div><input type=hidden name=cx value=014021643941856155761:l5eihuescdw><input type=hidden name=ie value=UTF-8> <input type=search name=q size=25 placeholder="powered by Google™"> <input type=submit name=sa value=搜索></div></form>
<p>当前位置: <a href="index.html">首页</a> <span class=u>‣</span> <a href="table-of-contents.html#generators">深入 Python 3</a> <span class=u>‣</span>
<p id=level>难度级别: <span class=u title=intermediate>♦♦♢♢♢</span>
<h1>闭合 <i class=baa>与</i> 生成器</h1>
<blockquote class=q>
<p><span class=u>❝</span> My spelling is Wobbly. It’s good spelling but it Wobbles, and the letters get in the wrong places. <span class=u>❞</span><br>— Winnie-the-Pooh
</blockquote>
<p id=toc>
<h2 id=divingin>深入</h2>
<p class=f>出于传递所有理解的原因,我一直对语言非常着迷。我指的不是编程语言。好吧,是编程语言,但同时也是自然语言。使用英语。英语是一种七拼八凑的语言,它从德语、法语、西班牙语和拉丁语(等等)语言中借用了大量词汇。事实上,“借用”是不恰当的词汇,“掠夺”更加符合。或者也许叫“同化“——就像博格人(译注:根据维基百科资料,Borg 是《星际旅行》虚构宇宙中的一个种族,该译法未经原作者映证)。是的,我喜欢这样。<p class=c><code>我们就是博格人。你们的语言和词源特性将会被添加到我们自己的当中。抵抗是徒劳的。</code>
<p>在本章中,将开始学习复数名词。以及返回其它函数的函数、高级正则表达式和生成器。但首先,让我们聊聊如何生成复数名词。(如果还没有阅读<a href="regular-expressions.html">《正则表达式》一章</a>,现在也许是个好时机读一读。本章将假定您理解了正则表达式的基础,并迅速进入更高级的用法。)<p>如果在讲英语的国家长大,或在正规的学校学习过英语,您可能对下面的基本规则很熟悉 :<ul>
<li>如果某个单词以 S 、X 或 Z 结尾,添加 ES 。<i>Bass</i> 变成 <i>basses</i>, <i>fax</i> 变成 <i>faxes</i>,而 <i>waltz</i> 变成 <i>waltzes</i>。
</li><li>如果某个单词以发音的 H 结尾,加 ES;如果以不发音的 H 结尾,只需加上 S 。什么是发音的 H ?指的是它和其它字母组合在一起发出能够听到的声音。因此 <i>coach</i> 变成 <i>coaches</i> 而 <i>rash</i> 变成 <i>rashes</i>,因为在说这两个单词的时候,能够听到 CH 和 SH 的发音。但是 <i>cheetah</i> 变成 <i>cheetahs</i>,因为 H 不发音。</li><li>如果某个单词以发 I 音的字母 Y 结尾,将 Y 改成 IES;如果 Y 与某个原因字母组合发其它音的话,只需加上 S 。因此 <i>vacancy</i> 变成 <i>vacancies</i>,但 <i>day</i> 变成 <i>days</i> 。</li><li>如果所有这些规则都不适用,只需加上 S 并作最好的打算。</li></ul>
<p>(我知道,还有许多例外情况。<i>Man</i> 变成 <i>men</i> 而 <i>woman</i> 变成 <i>women</i>,但是 <i>human</i> 变成 <i>humans</i>。<i>Mouse</i> 变成 <i>mice</i> ; <i>louse</i> 变成 <i>lice</i>,但 <i>house</i> 变成 <i>houses</i>。<i>Knife</i> 变成 <i>knives</i> ;<i>wife</i> 变成 <i>wives</i>,但是 <i>lowlife</i> 变成 <i>lowlifes</i>。而且甚至我还没有开始提到那些原型和复数形式相同的单词,就像 <i>sheep</i>、 <i>deer</i> 和 <i>haiku</i>。)<p>其它语言,当然是完全不同的。<p>让我们设计一个 Python 类库用来自动进行英语名词的复数形式转换。我们将以这四条规则为起点,但要记住的不可避免地还要增加更多规则。<p class=a>⁂
<h2 id=i-know>我知道,让我们用正则表达式!</h2>
<p>因此,您正在看着单词,至少是英语单词,也就是说您正在看着字符的字符串。规则说你必须找到不同的字符组合,然后进行不同的处理。这听起来是正则表达式的工作!<p class=d>[<a href="examples/plural1.py">下载 <code>plural1.py</code></a>]<pre class=pp><code>import re
def plural(noun):
<a> if re.search('[sxz]$', noun): <span class=u>①</span></a>
<a> return re.sub('$', 'es', noun) <span class=u>②</span></a>
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'</code></pre>
<ol>
<li>这是一条正则表达式,但它使用了在 <a href="regular-expressions.html"><i>《正则表达式》</i></a> 一章中没有讲过的语法。中括号表示“匹配这些字符的其中之一”。因此 <code>[sxz]</code> 的意思是: “<code>s</code>、 <code>x</code> 或 <code>z</code>”,但只匹配其中之一。对 <code>$</code> 应该很熟悉了,它匹配字符串的结尾。经过组合,该正则表达式将测试 <var>noun</var> 是否以 <code>s</code>、 <code>x</code> 或 <code>z</code> 结尾。</li><li>该 <code>re.sub()</code> 函数执行基于正则表达式的字符串替换。</li></ol>
<p>让我们看看正则表达式替换的细节。<pre class=screen>
<samp class=p>>>> </samp><kbd class=pp>import re</kbd>
<a><samp class=p>>>> </samp><kbd class=pp>re.search('[abc]', 'Mark')</kbd> <span class=u>①</span></a>
<_sre.SRE_Match object at 0x001C1FA8>
<a><samp class=p>>>> </samp><kbd class=pp>re.sub('[abc]', 'o', 'Mark')</kbd> <span class=u>②</span></a>
<samp class=pp>'Mork'</samp>
<a><samp class=p>>>> </samp><kbd class=pp>re.sub('[abc]', 'o', 'rock')</kbd> <span class=u>③</span></a>
<samp class=pp>'rook'</samp>
<a><samp class=p>>>> </samp><kbd class=pp>re.sub('[abc]', 'o', 'caps')</kbd> <span class=u>④</span></a>
<samp class=pp>'oops'</samp></pre>
<ol>
<li>字符串 <code>Mark</code> 包含 <code>a</code>、 <code>b</code> 或 <code>c</code> 吗?是的,它包含 <code>a</code> 。</li><li>好了,现在查找 <code>a</code>、 <code>b</code> 或 <code>c</code>,并将其替换为 <code>o</code>。<code>Mark</code> 变成了 <code>Mork</code>。</li><li>同一函数将 <code>rock</code> 转换为 <code>rook</code> 。</li><li>您可能会认为该函数会将 <code>caps</code> 转换为 <code>oaps</code>,但实际上并是这样。<code>re.sub</code> 替换 <em>所有的</em> 匹配项,而不仅仅是第一个匹配项。因此该正则表达式将 <code>caps</code> 转换为 <code>oops</code>,因为无论是 <code>c</code> 还是 <code>a</code> 均被转换为 <code>o</code> 。</li></ol>
<p>接下来,回到 <code>plural()</code> 函数……<pre class=pp><code>def plural(noun):
if re.search('[sxz]$', noun):
<a> return re.sub('$', 'es', noun) <span class=u>①</span></a>
<a> elif re.search('[^aeioudgkprt]h$', noun): <span class=u>②</span></a>
return re.sub('$', 'es', noun)
<a> elif re.search('[^aeiou]y$', noun): <span class=u>③</span></a>
return re.sub('y$', 'ies', noun)
else:
return noun + 's'</code></pre>
<ol>
<li>此处将字符串的结尾(通过 <code>$</code> 匹配)替换为字符串 <code>es</code> 。换句话来说,向字符串尾部添加一个 <code>es</code> 。可以通过字符串链接来完成同样的变化,例如 <code>noun + 'es'</code>,但我对每条规则都选用正则表达式,其原因将在本章稍后更加清晰。</li><li>仔细看看,这里出现了新的变化。作为方括号中的第一个字符, <code>^</code> 有特别的含义:非。<code>[^abc]</code> 的意思是:“ <em>除了</em> <code>a</code>、 <code>b</code> 或 <code>c</code> 之外的任何字符”。因此 <code>[^aeioudgkprt]</code> 的意思是除了 <code>a</code>、 <code>e</code>、 <code>i</code>、 <code>o</code>、 <code>u</code>、 <code>d</code>、 <code>g</code>、 <code>k</code>、 <code>p</code>、<code>r</code> 或 <code>t</code> 之外的任何字符。然后该字符必须紧随一个 <code>h</code>,其后是字符串的结尾。所匹配的是以 H 结尾且 H 发音的单词。</li><li>此处有同样的模式:匹配以 Y 结尾的单词,而 Y 之前的字符 <em>不是</em> <code>a</code>、 <code>e</code>、 <code>i</code>、 <code>o</code> 或 <code>u</code>。所匹配的是以 Y 结尾,且 Y 发音听起来像 I 的单词。</li></ol>
<p>让我们看看“否定”正则表达式的更多细节。<pre class=screen>
<samp class=p>>>> </samp><kbd class=pp>import re</kbd>
<a><samp class=p>>>> </samp><kbd class=pp>re.search('[^aeiou]y$', 'vacancy')</kbd> <span class=u>①</span></a>
<_sre.SRE_Match object at 0x001C1FA8>
<a><samp class=p>>>> </samp><kbd class=pp>re.search('[^aeiou]y$', 'boy')</kbd> <span class=u>②</span></a>
<samp class=p>>>> </samp>
<samp class=p>>>> </samp><kbd class=pp>re.search('[^aeiou]y$', 'day')</kbd>
<samp class=p>>>> </samp>
<a><samp class=p>>>> </samp><kbd class=pp>re.search('[^aeiou]y$', 'pita')</kbd> <span class=u>③</span></a>
<samp class=p>>>> </samp></pre>
<ol>
<li><code>vacancy</code> 匹配该正则表达式,因为它以 <code>cy</code> 结尾,且 <code>c</code> 并非 <code>a</code>、 <code>e</code>、 <code>i</code>、 <code>o</code> 或 <code>u</code>。</li><li><code>boy</code> 不匹配,因为它以 <code>oy</code> 结尾,可以明确地说 <code>y</code> 之前的字符不能是 <code>o</code> 。<code>day</code> 不匹配,因为它以 <code>ay</code> 结尾。</li><li><code>pita</code> 不匹配,因为它不以 <code>y</code> 结尾。</li></ol>
<pre class=screen>
<a><samp class=p>>>> </samp><kbd class=pp>re.sub('y$', 'ies', 'vacancy')</kbd> <span class=u>①</span></a>
<samp class=pp>'vacancies'</samp>
<samp class=p>>>> </samp><kbd class=pp>re.sub('y$', 'ies', 'agency')</kbd>
<samp class=pp>'agencies'</samp>
<a><samp class=p>>>> </samp><kbd class=pp>re.sub('([^aeiou])y$', r'\1ies', 'vacancy')</kbd> <span class=u>②</span></a>
<samp class=pp>'vacancies'</samp></pre>
<ol>
<li>该正则表达式将 <code>vacancy</code> 转换为 <code>vacancies</code> ,将 <code>agency</code> 转换为 <code>agencies</code>,这正是想要的结果。注意,它也会将 <code>boy</code> 转换为 <code>boies</code>,但这永远也不会在函数中发生,因为我们首先进行了 <code>re.search</code> 以找出永远不应进行该 <code>re.sub</code> 操作的单词。</li><li>顺便,我还想指出可以将该两条正则表达式合并起来(一条查找是否应用该规则,另一条实际应用规则),使其成为一条正则表达式。它看起来是下面这个样子:其中多数内容看起来应该很熟悉:使用了在 <a href="regular-expressions.html#phonenumbers">案例研究:分析电话号码</a> 中用到的记忆分组。该分组用于保存字母 <code>y</code> 之前的字符。然后在替换字符串中,用到了新的语法: <code>\1</code>,它表示“嘿,记住的第一个分组呢?把它放到这里。”在此例中, 记住了 <code>y</code> 之前的 <code>c</code> ,在进行替换时,将用 <code>c</code> 替代 <code>c</code>,用 <code>ies</code> 替代 <code>y</code> 。(如果有超过一个的记忆分组,可以使用 <code>\2</code> 和 <code>\3</code> 等等。)</li></ol>
<p>正则表达式替换功能非常强大,而 <code>\1</code> 语法则使之愈加强大。但是,将整个操作组合成一条正则表达式也更难阅读,而且也没有直接映射到刚才所描述的复数规则。刚才所阐述的规则,像 “如果单词以 S 、X 或 Z 结尾,则添加 ES 。”如果查看该函数,有两行代码都在表述“如果以 S 、X 或 Z 结尾,那么添加 ES 。”它没有之前那种模式更直接。<p class=a>⁂
<h2 id=a-list-of-functions>函数列表</h2>
<p>现在要增加一些抽象层次的内容。我们开始时定义了一系列规则:如果这样,那样做;否则前往下一条规则。现在让我们对部分程序进行临时的复杂化,以简化另一部分。<p class=d>[<a href="examples/plural2.py">download <code>plural2.py</code></a>]
<pre class=pp><code>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)
<a>def match_y(noun): <span class=u>①</span></a>
return re.search('[^aeiou]y$', noun)
<a>def apply_y(noun): <span class=u>②</span></a>
return re.sub('y$', 'ies', noun)
def match_default(noun):
return True
def apply_default(noun):
return noun + 's'
<a>rules = ((match_sxz, apply_sxz), <span class=u>③</span></a>
(match_h, apply_h),
(match_y, apply_y),
(match_default, apply_default)
)
def plural(noun):
<a> for matches_rule, apply_rule in rules: <span class=u>④</span></a>
if matches_rule(noun):
return apply_rule(noun)</code></pre>
<ol>
<li>现在,每条匹配规则都有自己的函数,它们返回对 <code>re.search()</code> 函数调用结果。</li><li>每条应用规则也都有自己的函数,它们调用 <code>re.sub()</code> 函数以应用恰当的复数变化规则。</li><li>现在有了一个 <code>rules</code> 数据结构——一个函数对的序列,而不是一个函数(<code>plural()</code>)实现多个条规则。</li><li>由于所有的规则被分割成单独的数据结构,新的 <code>plural()</code> 函数可以减少到几行代码。使用 <code>for</code> 循环,可以一次性从 <var>rules</var> 这个数据结构中取出匹配规则和应用规则这两样东西(一条匹配对应一条应用)。在 <code>for</code> 循环的第一次迭代过程中, <var>matches_rule</var> 将获取 <code>match_sxz</code>,而 <var>apply_rule</var> 将获取 <code>apply_sxz</code>。在第二次迭代中(假定可以进行到这一步), <var>matches_rule</var> 将会赋值为 <code>match_h</code>,而 <var>apply_rule</var> 将会赋值为 <code>apply_h</code> 。该函数确保最终能够返回某个值,因为终极匹配规则 (<code>match_default</code>) 只返回 <code>True</code>,意思是对应的应用规则 (<code>apply_default</code>) 将总是被应用。</li></ol>
<aside>变量 “rules” 是一系列函数对。</aside>
<p>该技术能够成功运作的原因是 <a href="your-first-python-program.html#everythingisanobject">Python 中一切都是对象</a>,包括了函数。数据结构 <var>rules</var> 包含了函数——不是函数的名称,而是实际的函数对象。在 <code>for</code> 循环中被赋值后,<var>matches_rule</var> 和 <var>apply_rule</var> 是可实际调用的函数。在第一次 <code>for</code> 循环的迭代过程中,这相当于调用 <code>matches_sxz(noun)</code>,如果返回一个匹配值,将调用 <code>apply_sxz(noun)</code> 。<p>如果这种附加抽象层令你迷惑,可以试着展开函数以了解其等价形式。整个 <code>for</code> 循环等价于下列代码:<pre class='nd pp'><code>
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)</code></pre>
<p>这段代码的好处是 <code>plural()</code> 函数被简化了。它处理一系列其它地方定义的规则,并以通用的方式对它们进行迭代。<ol>
<li>获取某匹配规则</li><li>是否匹配?然后调用应用规则,并返回结果。</li><li>不匹配?返回步骤 1 。</li></ol>
<p>这些规则可在任何地方以任何方式定义。<code>plural()</code> 函数并不关心。<p>现在,新增的抽象层是否值得呢?嗯,还没有。让我们考虑下要向函数中新增一条规则时该如何操作。在第一例中,将需要新增一条 <code>if</code> 语句到 <code>plural()</code> 函数中。在第二例中,将需要新增两个函数, <code>match_foo()</code> 和 <code>apply_foo()</code>,然后更新 <var>rules</var> 序列以指定新的匹配和应用函数按照其它规则按顺序调用。<p>但是对于下一节来说,这只是一个跳板而已。让我们继续……<p class=a>⁂
<h2 id=a-list-of-patterns>匹配模式列表</h2>
<p>其实并不是真的有必要为每个匹配和应用规则定义各自的命名函数。它们从未直接被调用,而只是被添加到 <var>rules</var> 序列并从该处被调用。此外,每个函数遵循两种模式的其中之一。所有的匹配函数调用 <code>re.search()</code>,而所有的应用函数调用 <code>re.sub()</code>。让我们将模式排除在考虑因素之外,使新规则定义更加简单。<p class=d>[<a href="examples/plural3.py">download <code>plural3.py</code></a>]
<pre class=pp><code>import re
def build_match_and_apply_functions(pattern, search, replace):
<a> def matches_rule(word): <span class=u>①</span></a>
return re.search(pattern, word)
<a> def apply_rule(word): <span class=u>②</span></a>
return re.sub(search, replace, word)
<a> return (matches_rule, apply_rule) <span class=u>③</span></a></code></pre>
<ol>
<li><code>build_match_and_apply_functions()</code> 函数用于动态创建其它函数。它接受 <var>pattern</var>、 <var>search</var> 和 <var>replace</var> 三个参数,并定义了 <code>matches_rule()</code> 函数,该函数通过传给 <code>build_match_and_apply_functions()</code> 函数的 <var>pattern</var> 及传递给所创建的 <code>matchs_rules()</code> 函数的 <var>word</var> 调用 <code>re.search()</code> 函数,哇。</li><li>应用函数的创建工作采用了同样的方式。应用函数只接受一个参数,并使用传递给 <code>build_match_and_apply_functions()</code> 函数的 <var>search</var> 和 <var>replace</var> 参数、以及传递给要创建 <code>apply_rule()</code> 函数的 <var>word</var> 调用 <code>re.sub()</code>。在动态函数中使用外部参数值的技术称为 <em>闭合【closures】</em>。基本上,常量的创建工作都在创建应用函数过程中完成:它接受一个参数 (<var>word</var>),但实际操作还加上了另外两个值(<var>search</var> 和 <var>replace</var>),该两个值都在定义应用函数时进行设置。</li><li>最后,<code>build_match_and_apply_functions()</code> 函数返回一个包含两个值的元组:即刚才所创建的两个函数。在这些函数中定义的常量( <code>match_rule()</code> 函数中的 <var>pattern</var> 函数,<code>apply_rule()</code> 函数中的 <var>search</var> 和 <var>replace</var> )与这些函数呆在一起,即便是在从 <code>build_match_and_apply_functions()</code> 中返回后也一样。这真是非常酷的一件事情。</li></ol>
<p>但如果此方式导致了难以置信的混乱(应该是这样,它确实有点奇怪),在看看如何使用之后可能会清晰一些。<pre class=pp><code><a>patterns = \ <span class=u>①</span></a>
(
('[sxz]$', '$', 'es'),
('[^aeioudgkprt]h$', '$', 'es'),
('(qu|[^aeiou])y$', 'y$', 'ies'),
<a> ('$', '$', 's') <span class=u>②</span></a>
)
<a>rules = [build_match_and_apply_functions(pattern, search, replace) <span class=u>③</span></a>
for (pattern, search, replace) in patterns]</code></pre>
<ol>
<li>我们的复数形式“规则”现在被定义为 <em>字符串</em> 的元组的元组(而不是函数)。每个组的第一个字符串是在 <code>re.search()</code> 中用于判断该规则是否匹配的正则表达式。各组中的第二和第三个字符串是在 <code>re.sub()</code> 中将实际用于使用规则将名词转换为复数形式的搜索和替换表达式。</li><li>此处的后备规则略有变化。在前例中,<code>match_default()</code> 函数仅返回 <code>True</code>,意思是如果更多的指定规则无一匹配,代码将简单地向给定词汇的尾部添加一个 <code>s</code>。本例则进行了一些功能等同的操作。最后的正则表达式询问单词是否有一个结尾(<code>$</code> 匹配字符串的结尾)。当然,每个字符串都有一个结尾,甚至是空字符串也有,因此该规则将始终被匹配。因此,它实现了 <code>match_default()</code> 函数同样的目的,始终返回 <code>True</code>:它确保了如果没有更多的指定规则用于匹配,代码将向给定单词的尾部增加一个 <code>s</code> 。</li><li>本行代码非常神奇。它以 <var>patterns</var> 中的字符串序列为参数,并将其转换为一个函数序列。怎么做到的?通过将字符串“映射”到 <code>build_match_and_apply_functions()</code> 函数。也就是说,它接受每组三重字符串为参数,并将该三个字符串作为实参调用 <code>build_match_and_apply_functions()</code> 函数。 <code>build_match_and_apply_functions()</code> 函数返回一个包含两个函数的元组。也就是说该 <var>规则</var> 最后的结尾与前例在功能上是等价的:一个元组列表,每个元组都是一对函数。第一个函数是调用 <code>re.search()</code> 的匹配函数;而第二个函数调用 <code>re.sub()</code> 的应用函数。</li></ol>
<p>此版本脚本的最前面是主入口点—— <code>plural()</code> 函数。<pre class=pp><code>def plural(noun):
<a> for matches_rule, apply_rule in rules: <span class=u>①</span></a>
if matches_rule(noun):
return apply_rule(noun)</code></pre>
<ol>
<li>由于 <var>规则</var> 列表与前例中的一样(实际上确实相同),因此毫不奇怪 <code>plural()</code> 函数基本没有发生变化。它是完全通用的,它以规则函数列表为参数,并按照顺序调用它们。它并不关系规则是如何定义的。在前例中,它们被定义为各自命名的函数。现在它们通过将 <code>build_match_and_apply_functions()</code> 函数的输出映射为源字符串的列表来动态创建。这没有任何关系; <code>plural()</code> 函数将以同样方式运作。</li></ol>
<p class=a>⁂
<h2 id=a-file-of-patterns>匹配模式文件</h2>
<p>目前,已经排除了重复代码,增加了足够的抽象性,因此复数形式规则可以字符串列表的形式进行定义。下一个逻辑步骤是将这些字符串放入一个单独的文件中,因此可独立于使用它们的代码来进行维护。<p>首先,让我们创建一份包含所需规则的文本文件。没有花哨的数据结构,只有空白符分隔的三列字符串。将其命名为 <code>plural4-rules.txt</code>.<p class=d>[<a href="examples/plural4-rules.txt">download <code>plural4-rules.txt</code></a>]
<pre class='nd pp'><code>[sxz]$ $ es
[^aeioudgkprt]h$ $ es
[^aeiou]y$ y$ ies
$ $ s</code></pre>
<p>下面看看如何使用该规则文件。<p class=d>[<a href="examples/plural4.py">download <code>plural4.py</code></a>]
<pre class=pp><code>import re
<a>def build_match_and_apply_functions(pattern, search, replace): <span class=u>①</span></a>
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 = []
<a>with open('plural4-rules.txt', encoding='utf-8') as pattern_file: <span class=u>②</span></a>
<a> for line in pattern_file: <span class=u>③</span></a>
<a> pattern, search, replace = line.split(None, 3) <span class=u>④</span></a>
<a> rules.append(build_match_and_apply_functions( <span class=u>⑤</span></a>
pattern, search, replace))</code></pre>
<ol>
<li><code>build_match_and_apply_functions()</code> 函数没有发生变化。仍然使用了闭合技术:通过外部函数中定义的变量来动态创建两个函数。</li><li>全局的 <code>open()</code> 函数打开文件并返回一个文件对象。此例中,将要打开的文件包含了名词复数形式的模式字符串。<code>with</code> 语句创建了叫做 <i>context【上下文】</i>的东西:当 <code>with</code> 块结束时,Python 将自动关闭文件,即便是在 <code>with</code> 块中引发了例外也会这样。在 <a href="files.html">《文件》</a> 一章中将学到关于 <code>with</code> 块和文件对象的更多内容。</li><li><code>for line in <fileobject></code> 代码从打开的文件中读取数据,并将文本赋值给 <var>line</var> 变量。在 <a href="files.html">《文件》</a> 一章中将学到更多关于读取文件的内容。</li><li>文件中每行都有三个值,单它们通过空白分隔(制表符或空白,没有区别)。要将它们分开,可使用字符串方法 <code>split()</code> 。<code>split()</code> 方法的第一个参数是 <code>None</code>,表示“对任何空白字符进行分隔(制表符或空白,没有区别)”。第二个参数是 <code>3</code>,意思是“针对空白分隔三次,丢弃该行剩下的部分。”像 <code>[sxz]$ $ es</code> 这样的行将被分割为列表 <code>['[sxz]$', '$', 'es']</code>,意思是 <var>pattern</var> 获得值 <code>'[sxz]$'</code>, <var>search</var> 获得值 <code>'$'</code>,而 <var>replace</var> 获得值 <code>'es'</code>。对于短短的一行代码来说确实威力够大的。</li><li>最后,将 <code>pattern</code> 、 <code>search</code> 和 <code>replace</code> 传入 <code>build_match_and_apply_functions()</code> 函数,它将返回一个函数的元组。将该元组添加到 <var>rules</var> 列表,最终 <var>rules</var> 将储存 <code>plural()</code> 函数所预期的匹配和应用函数列表。</li></ol>
<p>此处的改进是将复数形式规则独立地放到了一份外部文件中,因此可独立于使用它的代码单独对规则进行维护。代码是代码,数据是数据,生活更美好。<p class=a>⁂
<h2 id=generators>生成器</h2>
<p>如果有个通用 <code>plural()</code> 函数解析规则文件不就更棒了吗?获取规则,检查匹配,应用相应的转换,进入下一条规则。这是 <code>plural()</code> 函数所必须完成的事,也是 <code>plural()</code> 函数必须做的事。<p class=d>[<a href="examples/plural5.py">download <code>plural5.py</code></a>]
<pre class='nd pp'><code>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))</code></pre>
<p><em>这段</em>代码到底是如何运作的?让我们先看一个交互式例子。<pre class=screen>
<samp class=p>>>> </samp><kbd class=pp>def make_counter(x):</kbd>
<samp class=p>... </samp><kbd class=pp> print('entering make_counter')</kbd>
<samp class=p>... </samp><kbd class=pp> while True:</kbd>
<a><samp class=p>... </samp><kbd class=pp> yield x</kbd> <span class=u>①</span></a>
<samp class=p>... </samp><kbd class=pp> print('incrementing x')</kbd>
<samp class=p>... </samp><kbd class=pp> x = x + 1</kbd>
<samp class=p>... </samp>
<a><samp class=p>>>> </samp><kbd class=pp>counter = make_counter(2)</kbd> <span class=u>②</span></a>
<a><samp class=p>>>> </samp><kbd class=pp>counter</kbd> <span class=u>③</span></a>
<generator object at 0x001C9C10>
<a><samp class=p>>>> </samp><kbd class=pp>next(counter)</kbd> <span class=u>④</span></a>
<samp>entering make_counter
2</samp>
<a><samp class=p>>>> </samp><kbd class=pp>next(counter)</kbd> <span class=u>⑤</span></a>
<samp>incrementing x
3</samp>
<a><samp class=p>>>> </samp><kbd class=pp>next(counter)</kbd> <span class=u>⑥</span></a>
<samp>incrementing x
4</samp></pre>
<ol>
<li><code>make_counter</code> 中出现的 <code>yield</code> 命令的意思是这不是一个普通的函数。它是一次生成一个值的特殊类型函数。可以将其视为可恢复函数。调用该函数将返回一个可用于生成连续 <var>x</var> 值的 <i>生成器【Generator】</i>。</li><li>为创建 <code>make_counter</code> 生成器的实例,仅需像调用其它函数那样对它进行调用。注意该调用并不实际执行函数代码。可以这么说,是因为 <code>make_counter()</code> 函数的第一行调用了 <code>print()</code>,但实际并未打印任何内容。</li><li>该 <code>make_counter()</code> 函数返回了一个生成器对象。</li><li><code>next()</code> 函数以一个生成器对象为参数,并返回其下一个值。对 <var>counter</var> 生成器第一次调用 <code>next()</code> ,它针对第一条 <code>yield</code> 语句执行 <code>make_counter()</code> 中的代码,然后返回所产生的值。在此情况下,该代码输出将为 <code>2</code>,因其仅通过调用 <code>make_counter(2)</code> 对生成器进行初始创建。</li><li>对同一生成器对象反复调用 <code>next()</code> 将确切地从上次调用的位置开始继续,直到下一条 <code>yield</code> 语句。所有的变量、局部数据等内容在 <code>yield</code> 时被保存,在 <code>next()</code> 时被恢复。下一行代码等待被执行以调用 <code>print()</code> 以打印出 <samp>incrementing x</samp> 。之后,执行语句 <code>x = x + 1</code>。然后它继续通过 <code>while</code> 再次循环,而它再次遇上的第一条语句是 <code>yield x</code>,该语句将保存所有一切状态,并返回当前 <var>x</var> 的值(当前为 <code>3</code>)。</li><li>第二次调用 <code>next(counter)</code> 时,又进行了同样的工作,但这次 <var>x</var> 为 <code>4</code>。</li></ol>
<p>由于 <code>make_counter</code> 设置了一个无限循环,理论上可以永远执行该过程,它将不断递增 <var>x</var> 并输出数值。还是让我们看一个更加实用的生成器用法。<h3 id=a-fibonacci-generator>斐波那奇生成器</h3>
<aside>“yield” 暂停一个函数。“next()” 从其暂停处恢复其运行。</aside>
<p class=d>[<a href="examples/fibonacci.py">download <code>fibonacci.py</code></a>]
<pre class=pp><code>def fib(max):
<a> a, b = 0, 1 <span class=u>①</span></a>
while a < max:
<a> yield a <span class=u>②</span></a>
<a> a, b = b, a + b <span class=u>③</span></a></code></pre>
<ol>
<li>斐波那契序列是一系列的数字,每个数字都是其前两个数字之和。它从 0 和 <code>1</code> 开始,初始时上升缓慢,但越来越快。启动该序列需要两个变量:从 0 开始的 <var>a</var>,和从 <code>1</code> 开始的 <var>b</var> 。</li><li><var>a</var> 是当前序列中的数字,因此对它进行 yield 操作。</li><li><var>b</var> 是序列中下一个数字,因此将它赋值给 <var>a</var>,但同时计算下一个值 (<code>a + b</code>) 并将其赋值给 <var>b</var> 以供稍后使用。注意该步骤是并行发生的;如果 <var>a</var> 为 <code>3</code> 且 <var>b</var> 为 <code>5</code>,那么 <code>a, b = b, a + b</code> 将会把 <var>a</var> 设置 <code>5</code> (<var>b</var> 之前的值),将 <var>b</var> 设置为 <code>8</code> ( <var>a</var> 和 <var>b</var> 之前值的和)。</li></ol>
<p>因此,现在有了一个连续输出斐波那契数值的函数。当然,还可以使用递归来完成该功能,但这个方式更易于阅读。同样,它也与 <code>for</code> 循环合作良好。<pre class=screen>
<samp class=p>>>> </samp><kbd class=pp>from fibonacci import fib</kbd>
<a><samp class=p>>>> </samp><kbd class=pp>for n in fib(1000):</kbd> <span class=u>①</span></a>
<a><samp class=p>... </samp><kbd class=pp> print(n, end=' ')</kbd> <span class=u>②</span></a>
<samp class=pp>0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987</samp>
<a><samp class=p>>>> </samp><kbd class=pp>list(fib(1000))</kbd> <span class=u>③</span></a>
<samp class=pp>[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987]</samp></pre>
<ol>
<li>可以在 <code>for</code> 循环中直接使用像 <code>fib()</code> 这样的生成器。<code>for</code> 循环将会自动调用 <code>next()</code> 函数,从 <code>fib()</code> 生成器获取数值并赋值给 <code>for</code> 循环索引变量。(<var>n</var>)</li><li>每经过一次 <code>for</code> 循环, <var>n</var> 从 <code>fib()</code> 的 <code>yield</code> 语句获取一个新值,所需做的仅仅是输出它。一旦 <code>fib()</code> 的数字用尽(<var>a</var> 大于 <var>max</var>,即本例中的 <code>1000</code>), <code>for</code> 循环将会自动退出。</li><li>这是一个很有用的用法:将一个生成器传递给 <code>list()</code> 函数,它将遍历整个生成器(就像前例中的 <code>for</code> 循环)并返回所有数值的列表。</li></ol>
<h3 id=a-plural-rule-generator>复数规则生成器</h3>
<p>让我们回到 <code>plural5.py</code> 看看该版本的 <code>plural()</code> 函数是如何运作的。<pre class=pp><code>def rules(rules_filename):
with open(rules_filename, encoding='utf-8') as pattern_file:
for line in pattern_file:
<a> pattern, search, replace = line.split(None, 3) <span class=u>①</span></a>
<a> yield build_match_and_apply_functions(pattern, search, replace) <span class=u>②</span></a>
def plural(noun, rules_filename='plural5-rules.txt'):
<a> for matches_rule, apply_rule in rules(rules_filename): <span class=u>③</span></a>
if matches_rule(noun):
return apply_rule(noun)
raise ValueError('no matching rule for {0}'.format(noun))</code></pre>
<ol>
<li>此处没有太神奇的代码。由于规则文件中每行都靠包括以空白相间的三个值,因此使用 <code>line.split(None, 3)</code> 获取三个“列”的值并将它们赋值给三个局部变量。</li><li><em>然后使用了 yield。</em> 但生产了什么呢?通过老朋友—— <code>build_match_and_apply_functions()</code> 动态创建的两个函数,这与之前的例子是一样的。换而言之, <code>rules()</code> 是<em>按照需求</em>连续生成匹配和应用函数的生成器。</li><li>由于 <code>rules()</code> 是生成器,可直接在 <code>for</code> 循环中使用它。对 <code>for</code> 循环的第一次遍历,可以调用 <code>rules()</code> 函数打开模式文件,读取第一行,从该行的模式动态创建一个匹配函数和应用函数,然后生成动态创建的函数。对 <code>for</code> 循环的第二次遍历,将会精确地回到 <code>rules()</code> 中上次离开的位置(在 <code>for line in pattern_file</code> 循环的中间)。要进行的第一项工作是读取文件(仍处于打开状态)的下一行,基于该行的模式动态创建另一匹配和应用函数,然后生成两个函数。</li></ol>
<p>通过第四步获得了什么呢?启动时间。在第四步中引入 <code>plural4</code> 模块时,它读取了整个模式文件,并创建了一份所有可能规则的列表,甚至在考虑调用 <code>plural()</code> 函数之前。有了生成器,可以轻松地处理所有工作:可以读取规则,创建函数并试用它们,如果该规则可用甚至可以不读取文件剩下的部分或创建更多的函数。<p>失去了什么?性能!每次调用 <code>plural()</code> 函数,<code>rules()</code> 生成器将从头开始——这意味着重新打开模式文件,并从头开始读取,每次一行。<p>要是能够两全其美多好啊:最低的启动成本(无需对 <code>import</code> 执行任何代码),<em>同时</em> 最佳的性能(无需一次次地创建同一函数)。哦,还需将规则保存在单独的文件中(因为代码和数据要泾渭分明),还有就是永远不必两次读取同一行。<p>要实现该目标,必须建立自己的生成器。在进行<em>此工作</em>之前,必须对 Python 的类进行学习。<p class=a>⁂
<h2 id=furtherreading>深入阅读</h2>
<ul>
<li><a href=http://www.python.org/dev/peps/pep-0255/>PEP 255: 简单生成器</a>
</li><li><a href=http://effbot.org/zone/python-with-statement.htm>理解 Python 的 “with” 语句</a>
</li><li><a href=http://ynniv.com/blog/2007/08/closures-in-python.html>Python 中的闭合</a>
</li><li><a href=http://en.wikipedia.org/wiki/Fibonacci_number>斐波那契数值</a>
</li><li><a href=http://www2.gsu.edu/~wwwesl/egw/crump.htm>英语的不规则复数名词</a>
</li></ul>
<p class=v><a href="regular-expressions.html" rel=prev title="back to “Regular Expressions”"><span class=u>☜</span></a> <a href="iterators.html" rel=next title="onward to “Classes & Iterators”"><span class=u>☞</span></a>
<p class=c>© 2001–9 <a href="about.html">Mark Pilgrim</a>
<script src="j/jquery.js"></script>
<script src="j/prettify.js"></script>
<script src="j/dip3.js"></script>