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
/
refactoring.html
400 lines (346 loc) · 29 KB
/
refactoring.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
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
<!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 10}
</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=root value=搜索></div></form>
<p>当前位置: <a href="index.html">首页</a> <span class=u>‣</span> <a href="table-of-contents.html#refactoring">深入 Python 3</a> <span class=u>‣</span>
<p id=level>难度级别: <span class=u title=advanced>♦♦♦♦♢</span>
<h1>重构</h1>
<blockquote class=q>
<p><span class=u>❝</span> After one has played a vast quantity of notes and more notes, it is simplicity that emerges as the crowning reward of art. <span class=u>❞</span><br>— <a href=http://en.wikiquote.org/wiki/Fr%C3%A9d%C3%A9ric_Chopin>Frédéric Chopin</a>
</blockquote>
<p id=toc>
<h2 id=divingin>深入</h2>
<p class=f>就算是竭尽了全力编写全面的单元测试,还是会遇到错误。我所说的“错误”是什么意思?错误是尚未写到的测试实例。<pre class=screen><samp class=p>>>> </samp><kbd class=pp>import roman7</kbd>
<a><samp class=p>>>> </samp><kbd class=pp>roman7.from_roman('')</kbd> <span class=u>①</span></a>
<samp class=pp>0</samp></pre>
<ol>
<li>这就是错误。和其它无效罗马数字的一系列字符一样,空字符串将引发 <code>InvalidRomanNumeralError</code> 例外。</li></ol>
<p>在重现该错误后,应该在修复前写出一个导致该失败情形的测试实例,这样才能描述该错误。<pre class=pp><code>class FromRomanBadInput(unittest.TestCase):
.
.
.
def testBlank(self):
'''from_roman should fail with blank string'''
<a> self.assertRaises(roman6.InvalidRomanNumeralError, roman6.from_roman, '') <span class=u>①</span></a></code></pre>
<ol>
<li>这段代码非常简单。通过传入一个空字符串调用 <code>from_roman()</code> ,并确保其引发一个 <code>InvalidRomanNumeralError</code> 例外。难的是发现错误;找到了该错误之后对它进行测试是件轻松的工作。</li></ol>
<p>由于代码有错误,且有用于测试该错误的测试实例,该测试实例将会导致失败:<pre class='nd screen'>
<samp class=p>you@localhost:~/diveintopython3/examples$ </samp><kbd>python3 romantest8.py -v</kbd>
<samp>from_roman should fail with blank string ... FAIL
from_roman should fail with malformed antecedents ... ok
from_roman should fail with repeated pairs of numerals ... ok
from_roman should fail with too many repeated numerals ... ok
from_roman should give known result with known input ... ok
to_roman should give known result with known input ... ok
from_roman(to_roman(n))==n for all n ... ok
to_roman should fail with negative input ... ok
to_roman should fail with non-integer input ... ok
to_roman should fail with large input ... ok
to_roman should fail with 0 input ... ok
======================================================================
FAIL: from_roman should fail with blank string
----------------------------------------------------------------------
Traceback (most recent call last):
File "romantest8.py", line 117, in test_blank
self.assertRaises(roman8.InvalidRomanNumeralError, roman8.from_roman, '')
<mark>AssertionError: InvalidRomanNumeralError not raised by from_roman</mark>
----------------------------------------------------------------------
Ran 11 tests in 0.171s
FAILED (failures=1)</samp></pre>
<p><em>现在</em> 可以修复该错误了。<pre class=pp><code>def from_roman(s):
'''convert Roman numeral to integer'''
<a> if not s: <span class=u>①</span></a>
raise InvalidRomanNumeralError('Input can not be blank')
if not re.search(romanNumeralPattern, s):
<a> raise InvalidRomanNumeralError('Invalid Roman numeral: {}'.format(s)) <span class=u>②</span></a>
result = 0
index = 0
for numeral, integer in romanNumeralMap:
while s[index:index+len(numeral)] == numeral:
result += integer
index += len(numeral)
return result</code></pre>
<ol>
<li>只需两行代码:一行明确地对空字符串进行检查,另一行为 <code>raise</code> 语句。</li><li>在本书中还尚未提到该内容,因此现在让我们讲讲 <a href="strings.html#formatting-strings">字符串格式化</a> 最后一点内容。从 Python 3.1 起,在格式化标示符中使用位置索引时可以忽略数字。也就是说,无需使用格式化标示符 <code>{0}</code> 来指向 <code>format()</code> 方法的第一个参数,只需简单地使用 <code>{}</code> 而 Python 将会填入正确的位置索引。该规则适用于任何数量的参数;第一个 <code>{}</code> 代表 <code>{0}</code>,第二个 <code>{}</code> 代表 <code>{1}</code>,以此类推。</li></ol>
<pre class=screen>
<samp class=p>you@localhost:~/diveintopython3/examples$ </samp><kbd>python3 romantest8.py -v</kbd>
<a><samp>from_roman should fail with blank string ... ok</samp> <span class=u>①</span></a>
<samp>from_roman should fail with malformed antecedents ... ok
from_roman should fail with repeated pairs of numerals ... ok
from_roman should fail with too many repeated numerals ... ok
from_roman should give known result with known input ... ok
to_roman should give known result with known input ... ok
from_roman(to_roman(n))==n for all n ... ok
to_roman should fail with negative input ... ok
to_roman should fail with non-integer input ... ok
to_roman should fail with large input ... ok
to_roman should fail with 0 input ... ok
----------------------------------------------------------------------
Ran 11 tests in 0.156s
</samp>
<a><samp>OK</samp> <span class=u>②</span></a></pre>
<ol>
<li>现在空字符串测试实例通过了测试,也就是说错误被修正了。</li><li>所有其它测试实例仍然可以通过,说明该错误修正没有破坏其它部分。代码编写结束。</li></ol>
<p>用此方式编写代码将使得错误修正变得更困难。简单的错误(像这个)需要简单的测试实例;复杂的错误将会需要复杂的测试实例。在以测试为中心的环境中,由于必须在代码中精确地描述错误(编写测试实例),然后修正错误本身,看起来 <em>好像</em> 修正错误需要更多的时间。而如果测试实例无法正确地通过,则又需要找出到底是修正方案有错误,还数测试实例本身就有错误。然而从长远看,这种在测试代码和经测试代码之间的来回折腾是值得的,因为这样才更有可能在第一时间修正错误。同时,由于可以对新代码轻松地重新运行 <em>所有</em> 测试实例,在修正新代码时破坏旧代码的机会更低。今天的单元测试就是明天的回归测试。<p class=a>⁂
<h2 id=changing-requirements>控制需求变化</h2>
<p>为了获取准确的需求,尽管已经竭力将客户“钉”在原地,并经历了反复剪切、粘贴的痛苦,但需求仍然会变化。大多数客户在看到产品之前不知道自己想要什么,而且就算知道,他们也不擅长清晰地表述自己的想法。而即便擅长表述,他们在下一个版本中也会提出更多要求。因此,必须随时准备好更新测试实例以应对需求变化。<p>举个例子来说,假定我们要扩展罗马数字转换函数的能力范围。正常情况下,罗马数字中的任何一个字符在同一行中不得重复出现三次以上。但罗马人却愿意该规则有个例外:通过一行中的 4 个 <code>M</code> 字符来代表 <code>4000</code> 。进行该修改后,将会把可转换数字的范围从 <code>1..3999</code> 拓展为 <code>1..4999</code>。但首先必须对测试实例进行一些修改。<p class=d>[<a href="examples/roman8.py">download <code>roman8.py</code></a>]
<pre class=pp><code>class KnownValues(unittest.TestCase):
known_values = ( (1, 'I'),
.
.
.
(3999, 'MMMCMXCIX'),
<a> (4000, 'MMMM'), <span class=u>①</span></a>
(4500, 'MMMMD'),
(4888, 'MMMMDCCCLXXXVIII'),
(4999, 'MMMMCMXCIX') )
class ToRomanBadInput(unittest.TestCase):
def test_too_large(self):
'''to_roman should fail with large input'''
<a> self.assertRaises(roman8.OutOfRangeError, roman8.to_roman, 5000) <span class=u>②</span></a>
.
.
.
class FromRomanBadInput(unittest.TestCase):
def test_too_many_repeated_numerals(self):
'''from_roman should fail with too many repeated numerals'''
<a> for s in ('MMMMM', 'DD', 'CCCC', 'LL', 'XXXX', 'VV', 'IIII'): <span class=u>③</span></a>
self.assertRaises(roman8.InvalidRomanNumeralError, roman8.from_roman, s)
.
.
.
class RoundtripCheck(unittest.TestCase):
def test_roundtrip(self):
'''from_roman(to_roman(n))==n for all n'''
<a> for integer in range(1, 5000): <span class=u>④</span></a>
numeral = roman8.to_roman(integer)
result = roman8.from_roman(numeral)
self.assertEqual(integer, result)</code></pre>
<ol>
<li>现有的已知数值不会变(它们依然是合理的测试数值),但必须在 <code>4000</code> 范围之内(外)增加一些。在此,我已经添加了 <code>4000</code> (最短)、 <code>4500</code> (第二短)、 <code>4888</code> (最长) 和 <code>4999</code> (最大)。</li><li>“过大值输入” 的定义已经发生了变化。该测试用于通过传入 <code>4000</code> 调用 <code>to_roman()</code> 并期望引发一个错误;目前 <code>4000-4999</code> 是有效的值,必须将该值调整为 <code>5000</code> 。</li><li>“太多重复数字”的定义也发生了变化。该测试通过传入 <code>'MMMM'</code> 调用 <code>from_roman()</code> 并预期发生一个错误;目前 <code>MMMM</code> 被认定为有效的罗马数字,必须将该条件修改为 <code>'MMMMM'</code> 。</li><li>对范围内的每个数字进行完整循环测试,从 <code>1</code> 到 <code>3999</code>。由于范围已经进行了拓展,该 <code>for</code> 循环同样需要修改为以 <code>4999</code> 为上限。</li></ol>
<p>现在,测试实例已经按照新的需求进行了更新,但代码还没有,因按照预期,某些测试实例将返回失败结果。<pre class=screen>
<samp class=p>you@localhost:~/diveintopython3/examples$ </samp><kbd>python3 romantest9.py -v</kbd>
<samp>from_roman should fail with blank string ... ok
from_roman should fail with malformed antecedents ... ok
from_roman should fail with non-string input ... ok
from_roman should fail with repeated pairs of numerals ... ok
from_roman should fail with too many repeated numerals ... ok
<a>from_roman should give known result with known input ... ERROR <span class=u>①</span></a>
<a>to_roman should give known result with known input ... ERROR <span class=u>②</span></a>
<a>from_roman(to_roman(n))==n for all n ... ERROR <span class=u>③</span></a>
to_roman should fail with negative input ... ok
to_roman should fail with non-integer input ... ok
to_roman should fail with large input ... ok
to_roman should fail with 0 input ... ok
======================================================================
ERROR: from_roman should give known result with known input
----------------------------------------------------------------------
Traceback (most recent call last):
File "romantest9.py", line 82, in test_from_roman_known_values
result = roman9.from_roman(numeral)
File "C:\home\diveintopython3\examples\roman9.py", line 60, in from_roman
raise InvalidRomanNumeralError('Invalid Roman numeral: {0}'.format(s))
<mark>roman9.InvalidRomanNumeralError: Invalid Roman numeral: MMMM</mark>
======================================================================
ERROR: to_roman should give known result with known input
----------------------------------------------------------------------
Traceback (most recent call last):
File "romantest9.py", line 76, in test_to_roman_known_values
result = roman9.to_roman(integer)
File "C:\home\diveintopython3\examples\roman9.py", line 42, in to_roman
raise OutOfRangeError('number out of range (must be 0..3999)')
<mark>roman9.OutOfRangeError: number out of range (must be 0..3999)</mark>
======================================================================
ERROR: from_roman(to_roman(n))==n for all n
----------------------------------------------------------------------
Traceback (most recent call last):
File "romantest9.py", line 131, in testSanity
numeral = roman9.to_roman(integer)
File "C:\home\diveintopython3\examples\roman9.py", line 42, in to_roman
raise OutOfRangeError('number out of range (must be 0..3999)')
<mark>roman9.OutOfRangeError: number out of range (must be 0..3999)</mark>
----------------------------------------------------------------------
Ran 12 tests in 0.171s
FAILED (errors=3)</samp></pre>
<ol>
<li>一旦遇到 <code>'MMMM'</code>,<code>from_roman()</code> 已知值测试将会失败,因为 <code>from_roman()</code> 仍将其视为无效罗马数字。</li><li>一旦遇到 <code>4000</code>,<code>to_roman()</code> 已知值测试将会失败,因为 <code>to_roman()</code> 仍将其视为超范围数字。</li><li>而往返(译注:指在普通数字和罗马数字之间来回转换)检查遇到 <code>4000</code> 时也会失败,因为 <code>to_roman()</code> 仍认为其超范围。</li></ol>
<p>现在,我们有了一些由新需求导致失败的测试实例,可以考虑修正代码让它与新测试实例一致起来。(刚开始编写单元测试的时候,被测试代码绝不会在测试实例“之前”出现确实让人感觉有点怪。)尽管编码工作被置后安排,但还是不少要做的事情,一旦与测试实例相符,编码工作就可以结束了。一旦习惯单元测试后,您可能会对自己曾在编程时不进行测试感到很奇怪。)<p class=d>[<a href="examples/roman9.py">download <code>roman9.py</code></a>]
<pre class=pp><code>roman_numeral_pattern = re.compile('''
^ # beginning of string
<a> M{0,4} # thousands - 0 to 4 Ms <span class=u>①</span></a>
(CM|CD|D?C{0,3}) # hundreds - 900 (CM), 400 (CD), 0-300 (0 to 3 Cs),
# or 500-800 (D, followed by 0 to 3 Cs)
(XC|XL|L?X{0,3}) # tens - 90 (XC), 40 (XL), 0-30 (0 to 3 Xs),
# or 50-80 (L, followed by 0 to 3 Xs)
(IX|IV|V?I{0,3}) # ones - 9 (IX), 4 (IV), 0-3 (0 to 3 Is),
# or 5-8 (V, followed by 0 to 3 Is)
$ # end of string
''', re.VERBOSE)
def to_roman(n):
'''convert integer to Roman numeral'''
<a> if not (0 < n < 5000): <span class=u>②</span></a>
raise OutOfRangeError('number out of range (must be 1..4999)')
if not isinstance(n, int):
raise NotIntegerError('non-integers can not be converted')
result = ''
for numeral, integer in roman_numeral_map:
while n >= integer:
result += numeral
n -= integer
return result
def from_roman(s):
.
.
.</code></pre>
<ol>
<li>根本无需对 <code>from_roman()</code> 函数进行任何修改。唯一需要修改的是 <var>roman_numeral_pattern</var> 。仔细观察下,将会发现我已经在正则表达式的第一部分中将 <code>M</code> 字符的数量从 <code>3</code> 优化为 <code>4</code> 。该修改将允许等价于 <code>4999</code> 而不是 <code>3999</code> 的罗马数字。实际的 <code>from_roman()</code> 函数完全是通用的;它只查找重复的罗马数字字符并将它们加起来,而不关心它们重复了多少次。之前无法处理 <code>'MMMM'</code> 的唯一原因是我们通过正则表达式匹配明确地阻止了它这么做。</li><li><code>to_roman()</code> 函数只需在范围检查中进行一个小改动。将之前检查 <code>0 < n < 4000</code> 的地方现在修改为检查 <code>0 < n < 5000</code> 。同时修改 <code>引发</code> 的错误信息,以体现新的可接受范围 (<code>1..4999</code> 取代 <code>1..3999</code>) 。无需对函数剩下部分进行任何修改;它已经能够应对新的实例。(它将对找到的每个千位增加 <code>'M'</code> ;如果给定 <code>4000</code>,它将给出 <code>'MMMM'</code>。之前它不这么做的唯一原因是我们通过范围检查明确地阻止了它。)</li></ol>
<p>所需做的就是这两处小修改,但你可能会有点怀疑。嗨,别光听我说,你自己看看吧。<pre class='nd screen'>
<samp class=p>you@localhost:~/diveintopython3/examples$ </samp><kbd>python3 romantest9.py -v</kbd>
<samp>from_roman should fail with blank string ... ok
from_roman should fail with malformed antecedents ... ok
from_roman should fail with non-string input ... ok
from_roman should fail with repeated pairs of numerals ... ok
from_roman should fail with too many repeated numerals ... ok
from_roman should give known result with known input ... ok
to_roman should give known result with known input ... ok
from_roman(to_roman(n))==n for all n ... ok
to_roman should fail with negative input ... ok
to_roman should fail with non-integer input ... ok
to_roman should fail with large input ... ok
to_roman should fail with 0 input ... ok
----------------------------------------------------------------------
Ran 12 tests in 0.203s
<a>OK <span class=u>①</span></a></samp></pre>
<ol>
<li>所有测试实例均通过了。代码编写结束。</li></ol>
<p>全面单元测试的意思是:无需依赖某个程序员来说“相信我吧。”<p class=a>⁂
<h2 id=refactoring>重构</h2>
<p>关于全面单元测试,最美妙的事情不是在所有的测试实例通过后的那份心情,也不是别人抱怨你破坏了代码,而你通过实践 <em>证明</em> 自己没有时的快感。单元测试最美妙之处在于它给了你大刀阔斧进行重构的自由。<p>重构是修改可运作代码,使其表现更佳的过程。通常,“更佳”指的是“更快”,但它也可能指的是“占用更少内存“、”占用更少磁盘空间“或者”更加简洁”。对于你的环境、你的项目来说,无论重构意味着什么,它对程序的长期健康都至关重要。<p>本例中,“更佳”的意思既包括“更快”也包括“更易于维护”。具体而言,因为用于验证罗马数字的正则表达式生涩冗长,该 <code>from_roman()</code> 函数比我所希望的更慢,也更加复杂。现在,你可能会想,“当然,正则表达式就又臭又长的,难道我有其它办法验证任意字符串是否为罗马数字吗?”<p>答案是:只针对 5000 个数进行转换;为什么不知建立一个查询表呢?意识到 <em>根本不需要使用正则表达式</em> 之后,这个主意甚至变得更加理想了。在建立将整数转换为罗马数字的查询表的同时,还可以建立将罗马数字转换为整数的逆向查询表。在需要检查任意字符串是否是有效罗马数字的时候,你将收集到所有有效的罗马数字。“验证”工作简化为一个简单的字典查询。<p>最棒的是,你已经有了一整套单元测试。可以修改模块中一半以上的代码,而单元测试将会保持不变。这意味着可以向你和其他人证明:新代码运作和最初的一样好。<p class=d>[<a href="examples/roman10.py">download <code>roman10.py</code></a>]
<pre class=pp><code>class OutOfRangeError(ValueError): pass
class NotIntegerError(ValueError): pass
class InvalidRomanNumeralError(ValueError): pass
roman_numeral_map = (('M', 1000),
('CM', 900),
('D', 500),
('CD', 400),
('C', 100),
('XC', 90),
('L', 50),
('XL', 40),
('X', 10),
('IX', 9),
('V', 5),
('IV', 4),
('I', 1))
to_roman_table = [ None ]
from_roman_table = {}
def to_roman(n):
'''convert integer to Roman numeral'''
if not (0 < n < 5000):
raise OutOfRangeError('number out of range (must be 1..4999)')
if int(n) != n:
raise NotIntegerError('non-integers can not be converted')
return to_roman_table[n]
def from_roman(s):
'''convert Roman numeral to integer'''
if not isinstance(s, str):
raise InvalidRomanNumeralError('Input must be a string')
if not s:
raise InvalidRomanNumeralError('Input can not be blank')
if s not in from_roman_table:
raise InvalidRomanNumeralError('Invalid Roman numeral: {0}'.format(s))
return from_roman_table[s]
def build_lookup_tables():
def to_roman(n):
result = ''
for numeral, integer in roman_numeral_map:
if n >= integer:
result = numeral
n -= integer
break
if n > 0:
result += to_roman_table[n]
return result
for integer in range(1, 5000):
roman_numeral = to_roman(integer)
to_roman_table.append(roman_numeral)
from_roman_table[roman_numeral] = integer
build_lookup_tables()</code></pre>
<p>让我们打断一下,进行一些剖析工作。可以说,最重要的是最后一行:<pre class='nd pp'><code>build_lookup_tables()</code></pre>
<p>可以注意到这是一次函数调用,但没有 <code>if</code> 语句包裹住它。这不是 <code>if __name__ == '__main__'</code> 语块;<em>模块被导入时</em> 它将会被调用。(重要的是必须明白:模块将只被导入一次,随后被缓存了。如果导入一个已导入模块,将不会导致任何事情发生。因此这段代码将只在第一此导入时运行。)<p>那么,该 <code>build_lookup_tables()</code> 函数究竟进行了哪些操作呢?很高兴你问这个问题。<pre class=pp><code>to_roman_table = [ None ]
from_roman_table = {}
.
.
.
def build_lookup_tables():
<a> def to_roman(n): <span class=u>①</span></a>
result = ''
for numeral, integer in roman_numeral_map:
if n >= integer:
result = numeral
n -= integer
break
if n > 0:
result += to_roman_table[n]
return result
for integer in range(1, 5000):
<a> roman_numeral = to_roman(integer) <span class=u>②</span></a>
<a> to_roman_table.append(roman_numeral) <span class=u>③</span></a>
from_roman_table[roman_numeral] = integer</code></pre>
<ol>
<li>这是一段聪明的程序代码……也许过于聪明了。上面定义了 <code>to_roman()</code> 函数;它在查询表中查找值并返回结果。而 <code>build_lookup_tables()</code> 函数重定义了 <code>to_roman()</code> 函数用于实际操作(像添加查询表之前的例子一样)。在 <code>build_lookup_tables()</code> 函数内部,对 <code>to_roman()</code> 的调用将会针对该重定义的版本。一旦 <code>build_lookup_tables()</code> 函数退出,重定义的版本将会消失 — 它的定义只在 <code>build_lookup_tables()</code> 函数的作用域内生效。</li><li>该行代码将调用重定义的 <code>to_roman()</code> 函数,该函数实际计算罗马数字。</li><li>一旦获得结果(从重定义的 <code>to_roman()</code> 函数),可将整数及其对应的罗马数字添加到两个查询表中。</li></ol>
<p>查询表建好后,剩下的代码既容易又快捷。<pre class=pp><code>def to_roman(n):
'''convert integer to Roman numeral'''
if not (0 < n < 5000):
raise OutOfRangeError('number out of range (must be 1..4999)')
if int(n) != n:
raise NotIntegerError('non-integers can not be converted')
<a> return to_roman_table[n] <span class=u>①</span></a>
def from_roman(s):
'''convert Roman numeral to integer'''
if not isinstance(s, str):
raise InvalidRomanNumeralError('Input must be a string')
if not s:
raise InvalidRomanNumeralError('Input can not be blank')
if s not in from_roman_table:
raise InvalidRomanNumeralError('Invalid Roman numeral: {0}'.format(s))
<a> return from_roman_table[s] <span class=u>②</span></a></code></pre>
<ol>
<li>像前面那样进行同样的边界检查之后,<code>to_roman()</code> 函数只需在查询表中查找并返回适当的值。</li><li>同样,<code>from_roman()</code> 函数也缩水为一些边界检查和一行代码。不再有正则表达式。不再有循环。O(1) 转换为或转换到罗马数字。</li></ol>
<p>但这段代码可以运作吗?为什么可以,是的它可以。而且我可以证明。<pre class=screen>
<samp class=p>you@localhost:~/diveintopython3/examples$ </samp><kbd>python3 romantest10.py -v</kbd>
<samp>from_roman should fail with blank string ... ok
from_roman should fail with malformed antecedents ... ok
from_roman should fail with non-string input ... ok
from_roman should fail with repeated pairs of numerals ... ok
from_roman should fail with too many repeated numerals ... ok
from_roman should give known result with known input ... ok
to_roman should give known result with known input ... ok
from_roman(to_roman(n))==n for all n ... ok
to_roman should fail with negative input ... ok
to_roman should fail with non-integer input ... ok
to_roman should fail with large input ... ok
to_roman should fail with 0 input ... ok
----------------------------------------------------------------------
<a>Ran 12 tests in 0.031s <span class=u>①</span></a>
OK</samp></pre>
<ol>
<li>它不仅能够回答你的问题,还运行得非常快!好象速度提升了 10 倍。当然,这种比较并不公平,因为此版本在导入时耗时更长(在建造查询表时)。但由于只进行一次导入,启动的成本可以由对 <code>to_roman()</code> 和 <code>from_roman()</code> 函数的所有调用摊薄。由于该测试进行几千次函数调用(来回单独测试上万次),节省出来的效率成本得以迅速提升!</li></ol>
<p>这个故事的寓意是什么?<ul>
<li>简单是一种美德。</li><li>特别在涉及到正则表达式的时候。</li><li>单元测试令你在进行大规模重构时充满自信。</li></ul>
<p class=a>⁂
<h2 id=summary>摘要</h2>
<p>单元测试是一个威力强大的概念,如果正确实施,不但可以降低维护成本,还可以提高长期项目的灵活性。但同时还必须明白:单元测试既不是灵丹妙药,也不是解决问题的魔术,更不是银弹。编写良好的测试实例非常艰难,确保它们时刻保持最新必须成为一项纪律(特别在客户要求关键错误修正时)。单元测试不是功能测试、集成测试或用户承受能力测试等其它测试的替代品。但它是可行的、行之有效的,见识过其功用后,你将对之前曾没有用它而感到奇怪。<p>这几章覆盖的内容很多,很大一部分都不是 Python 所特有的。许多语言都有单元测试框架,但所有框架都要求掌握同一基本概念:<ul>
<li>设计测试实例是件具体、自动且独立的工作。</li><li>在编写被测试代码 <em>之前</em> 编写测试实例。</li><li>编写用于检查好输入并验证正确结果的测试</li><li>编写用于测试“坏”输入并做出正确失败响应的测试。</li><li>编写并更新测试实例以反映新的需求</li><li>毫不留情地重构以提升性能、可扩展性、可读性、可维护性及任何缺乏的特性。</li></ul>
<p class=v><a rel=prev href="unit-testing.html" title="back to “Unit Testing”"><span class=u>☜</span></a> <a href="files.html" rel=next title="onward to “Files”"><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>