-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathchapter07.tex
More file actions
496 lines (361 loc) · 15.6 KB
/
chapter07.tex
File metadata and controls
496 lines (361 loc) · 15.6 KB
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
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
\chapter{迭代器}
\index{iteration 迭代器}
\section{多重赋值}
\index{statement!assignment}
\index{mutiple assignment 多重赋值}
你可能已经发现,给一个变量多次赋值是合法的。一次新的赋值使得已存在的变量指向一个新值(当然也就不指向原来的值)。
\beforeverb
\begin{verbatim}
bruce = 5
print bruce,
bruce = 7
print bruce
\end{verbatim}
\afterverb
程序的输出是{\tt 5 7},因为第一次{\tt bruce}被输出时,它的值是5,第二次是7。
第一个{\tt print}语句末尾的逗号抑制了换行,这也是为什么两个输出在同一行的原因。
\index{newline 换行}
下图是多重赋值的状态图:
\index{state diagram}
\index{diagram!state}
\beforefig
\centerline{\includegraphics{figs/assign2.eps}}
\afterfig
对于多重赋值,很有必要分清赋值操作符和关系运算符中的等号。因为Python使用
等于号({\tt =})来表示赋值。很容易,误把这样的语句{\tt a = b}当作判断相等的语句,
实际不是的!\\
\index{相等与赋值}
第一,相等是对称关系,赋值不是。比如,在数学中,如果$a = 7$, 那么$7 =
a$。但在Python中,语句{\tt a = 7}是合法的,{\tt 7 = a}是非法的。\\
另外,在数学中,相等语句总是要么为真要么为假。如果,$a = b$,则,$a$总
是等于$b$。在Python中,赋值语句可以使得两个变量相等,但是他们不总是保持相等:
\beforeverb
\begin{verbatim}
a = 5
b = a # a and b are now equal
a = 3 # a and b are no longer equal
\end{verbatim}
\afterverb
%
第三行改变了{\tt a}的值,但是没有改变{\tt b}的值。所以他们不再相等。
尽管多重赋值通常是有益的,使用时也要小心。如果变量的值经常改变,代码
会变得很脑阅读和调试。
\section{更新变量}
\label{update}
\index{update 更新}
\index{variable!updating}
多重赋值的最常见的形式之一就是更新(update),变量的新值依赖于原有值。
\beforeverb
\begin{verbatim}
x = x+1
\end{verbatim}
\afterverb
含义是:获取{\tt x}的当前值,加一,然后用新值更新变量{\tt x}。
如果试着更新一个不存在的变量,将会得到一个错误,因为Python在把值赋给
{\tt x}之前会计算右边的值。
\beforeverb
\begin{verbatim}
>>> x = x+1
NameError: name 'x' is not defined
\end{verbatim}
\afterverb
%
在更新一个变量之前,必须得初始化(initialize)它,通常的做法就是一个
简单的赋值。
\index{initialization (before update) (更新前)初始化}
\beforeverb
\begin{verbatim}
>>> x = 0
>>> x = x+1
\end{verbatim}
\afterverb
%
仅仅通过加一来更新变量叫做增量 (increment);减一叫做减量(decrement)。
\index{increment 增量}
\index{decrement 减量}
\section{{\tt while 语句}}
\index{statement!while}
\index{while loop while循环}
\index{loop!while}
\index{iteration 迭代}
计算机通常被用来自动完成重复性的任务。计算机很擅长于重复相同或相似的任务。人类却恰恰相反\footnote{太枯燥了,回顾一下小学时,老师天天要求抄生字......}。
我们已经看到两个程序,{\tt countdown}和\verb"print_n",它们使用递归
实现重复,这也可以乘坐迭代{\bf iteration}。因为迭代是如此的常见,以致
于Python提供了几个特有的方式来简化使用。其中之一就是我们在\ref{重复}
部分看到的{\tt for}语句。我们不久将回来重新研究它。\\
另外一个就是{\tt while}语句。这里是一个使用{\tt while}语句的{\tt coutdown}版本。
\beforeverb
\begin{verbatim}
def countdown(n):
while n > 0:
print n
n = n-1
print 'Blastoff!'
\end{verbatim}
\afterverb
我们几乎可以把{\tt while}语句当成英语来读了。含义是:当{\tt n}大于0时
,显示{\tt n}的值,并把{\tt n}的值减1。当{\tt n}的值为0的时候,显示{\tt Blastoff!}。
\index{flow of execution 执行流}
更正式地,下面的是{\tt while}语句的执行流。
\begin{enumerate}
\item 计算条件的值,产生结果{\tt True}或者{\tt False}。
\item 如果条件为假,退出{\tt while循环},继续执行下一条语句。
\item 如果条件为真,执行语句体里的语句,然后回到步骤一。
\end{enumerate}
执行流的类型称作是循环(loop)的原因是因为第三步循环返回至第一步。
\index{condition 条件}
\index{loop 循环}
\index{body 体}
循环体应该改变一个或多个变量的值,使得最终条件为假,循环终止。否则,
循环将永远重复,也就产生了无限循环(infinite loop)。计算机科学家的
一个永远的谈资就是看到洗发水的说明"泡沫,漂洗,重复",是一个无限循环。
\index{infinite loop 无限循环}
\index{loop!infinite}
在{\tt countdown}的例子里,我们可以证明循环一定会终止,因为我们知道
{\tt n}的值是有限的,并且每一次循环{\tt n}的值都会减小,最终,{\tt n}的值肯定是0。在其他情况下,就不一定这么容易辨别了:
beforeverb
\begin{verbatim}
def sequence(n):
while n != 1:
print n,
if n%2 == 0: # n is even
n = n/2
else: # n is odd
n = n*3+1
\end{verbatim}
\afterverb
这个循环的条件是{\tt n != 1},所以循环会一直执行到{\tt n}是{\tt 1},
此时条件为假。\\
每一次循环,程序输出{\tt n}的值,然后检查是否为偶数或奇数。如果是偶数
{\tt n}就除以2。如果为奇数,{\tt n}的值就被{\tt n*3+1}代替。比如,
如果传递3给{\tt sequence},产生的结果是3, 10, 5, 16, 8, 4, 2, 1。
因为{\tt n}时增时减,没有一个明显的办法确定{\tt n}是否会为1,也就是
程序是否会正常终止。对于某些特别的{\tt n},我们可以证明终止。比如,
如果{\tt n}的值是2的倍数,每次循环时{\tt n}的值,都是偶数直到为1。
前面的例子从16开始都是这种情况。
\index{Collatz conjecture Collatz猜想}
困难的是我们是否可以证明对所有的正整数,程序都能终止。迄今为止\footnote{参看\url{wikipedia.org/wiki/Collatz_conjecture}.}没有人可以证明
可以,也没有人证明不可以。
\begin{ex}
重写\ref{递归}部分的\verb"print_n"函数,要求使用迭代器,而不是递归。
\end{ex}
\section{{\tt break}语句}
\index{break statement break语句}
\index{statement!break}
有时,直到执行到循环体里面的时候,才直到需要跳出循环。此时,我们可以
使用{\tt break}语句跳出循环。
比如,假设一直想从用户那里得到输入,直到用户输入{\tt done}。我们可以
这么写:
\beforeverb
\begin{verbatim}
while True:
line = raw_input('> ')
if line == 'done':
break
print line
print 'Done!'
\end{verbatim}
\afterverb
循环条件为{\tt True},也就是永远为真,所以循环直到遇到break statement
才终止执行。
每次循环,用尖括号提示用户。如果用户输入{\tt done},{\tt break}语句
终止了循环。否则,程序输出用户输入的内容,会到循环的顶部。下面是一个例子:
\beforeverb
\begin{verbatim}
> not done
not done
> done
Done!
\end{verbatim}
\afterverb
%
这种使用{\tt while}循环的方式很常见,因为我们可以在循环的任何地方检查
条件(不仅仅是在顶部),同时也积极的表达了结束的条件(当这个发生时,终止),而不是消极地("一直运行,直到这个发生")。
\section{平方根}
\index{square root}
循环经常用在计算数值的程序中,通常都是以一个相近的值开始,然后迭代,逐渐提高。
\index{Newton's method 牛顿方法}
比如,有一种计算平方根的算法叫牛顿方法。假设,我们想得到$a$的平方根。如果以任意猜测的
一个值开始,我们可以用下面的公式,计算一个更好的猜测值:
\[ y = \frac{x + a/x}{2} \]
比如,如果$a$是4, $x$是3:
\beforeverb
\begin{verbatim}
>>> a = 4.0
>>> x = 3.0
>>> y = (x + a/x) / 2
>>> print y
2.16666666667
\end{verbatim}
\afterverb
%
结果已经接近正确答案了($\sqrt{4} = 2$)。如果我们重复这个过程,就更接近了:
\beforeverb
\begin{verbatim}
>>> x = y
>>> y = (x + a/x) / 2
>>> print y
2.00641025641
\end{verbatim}
\afterverb
经过几次更新,猜测值基本上等于精确值了:
\index{update 更新}
\beforeverb
\begin{verbatim}
>>> x = y
>>> y = (x + a/x) / 2
>>> print y
2.00001024003
>>> x = y
>>> y = (x + a/x) / 2
>>> print y
2.00000000003
\end{verbatim}
\afterverb
一般来说,我们实现并不知道经过多少步才能得到正确的结果,但是我们知道什么时候得到
正确的结果,应为猜测值成定值了。
\beforeverb
\begin{verbatim}
>>> x = y
>>> y = (x + a/x) / 2
>>> print y
2.0
>>> x = y
>>> y = (x + a/x) / 2
>>> print y
2.0
\end{verbatim}
\afterverb
当{\tt y == x},我们就可以停止了。下面是一个循环,从一个初始值开始,然后逐步逼近,直到猜测值为定值。
\beforeverb
\begin{verbatim}
while True:
print x
y = (x + a/x) / 2
if y == x:
break
x = y
\end{verbatim}
\afterverb
%
对于大多数的{\tt a},这个都适用。但是,一般说来,测试浮点数是否相等是危险地。浮点值
只是近似相等:大多数有理数,像$1/3$,和无理数,像$\sqrt{2}$,不能用一个浮点数
精确的表示。
\index{floating-point 浮点}
\index{epsilon }
与其检查{\tt x}和{\tt y}是否精确相等,不如安全的采用内置的函数{\tt abs}计算
差的绝对值:
\beforeverb
\begin{verbatim}
if abs(y-x) < epsilon:
break
\end{verbatim}
\afterverb
%
这里,\verb"epsilon"是一个极小值,像{\tt 0.0000001} ,表示了什么样的接近才是
非常接近了。
\begin{ex}
\label{square_root}
\index{encapsulation 封装}
把这个循环封装在函数\verb"square_root"里,接受{\tt a}为参数,选择一个合适的{\tt x},返回{\tt a}的近似平方根。
\end{ex}
\section{算法}
\index{algorithm 算法}
牛顿方法是算法的一个例子:它是解决一类问题的方法(在这个例子里是计算平方根)。
定义一个算法可不是件容易的事。从不是算法的方面入手或许会有帮助。当你学习
单位数相乘时,你背了乘法表。事实上,你记住了100个具体的解法。这种知识不是
算法。\\
但是,如果你比较“懒”,你可能作些小弊。比如,计算$n$和9的乘积,你可以把十位写成$n-1$,个位写成$10-n$。这个就是解决任何单位数乘以9的一般方法\footnote{译注:比如
$8 * 9 = 72 = (8-1)(10-8)$}。对了,这就是算法。
\index{addition with carrying}
\index{carrying, addition with}
\index{subtraction!with borrowing}
\index{borrowing, subtraction with}
类似地,我们学到的技巧,比如加法进位,减法借位,长除法都是算法。这些算法的共有的
特点就是他们不需要任何的智力来实施。他们是机械的过程,每一步都依靠简单的规则。
从我的观点来看,人们花费大量的时间在学校里学习---不夸张地说,不需要任何智力的算法是非常不值得的。\\
从另一方面来说,设计算法的过程确实有趣的,挑战智力的,也是程序设计的中心内容。
有些事情,人们作起来很自然,没有困难也无须苦思冥想,但却是最难用算法表达的。理解自然语言是一个好的例子。我们都有这种能力,但是至今没有人能解释为什么我们可以,至少我们不能以算法的形式表达出来。
\section{调试}
当我们开始编写大型的程序时,就会发现调试将会花费我们大量的时间。代码越多,意味着
犯错的机会就越大,隐藏bug(s)的地方就越多。
\index{debugging!by bisection}
\index{bisection, debugging by}
减少调试时间的一种方法就是“二分法”。例如,程序有100行代码,每次检查一个,
需要100步。
换一种思路,把问题分成两半。查看程序的中间部分,或者接近中间部分,寻找一个
中间值来检查。添加一个{\tt print}语句(或者其他的能产生验证效果的语句),然后执行程序。
如果中间检查有问题,那么必定是程序的前半部分有问题。反之,则在第二部分。
每次按照这样检查的话,我们缩减了需要检查的代码。至少理论上来说,六步以后,
(这远远小于100),我们就可以缩减到一两行代码了。
实际中,很难界定程序的中间部分是哪里,也不总可能检查它。数代码的行数然后计算精确的中间点是没有任何意义的。相反,仔细想象什么地方最有可能出现错误,什么地方容易插入一个语句来检查。然后,选择一处你认为bug出现在检查点前后几率近似相等的地方。
\section{术语表}
\begin{description}
\item [multiple assignment 多重赋值:] 在程序的执行过程中给同一个变量赋多次值。
\index{mutiple assignement 多重赋值}
\index{assignment!multiple}
\item [update 更新:] 变量的新值依赖原来值的赋值。
\index{update 更新}
\item [initialization 初始化:]给一个将被更新的变量初始值的赋值。
\index{initialization!variable}
\item [increment 增量:] 增加一个变量的更新(通常是1)。
\index{increment}
\item [decrement 减量:]减小一个变量的更新(通常是1)。
\index{decrement}
\item [iteration 迭代:]使用递归或者循环的重复性执行的语句集合。
\index{iteration 迭代}
\item [infinite loop:无限循环]终止条件永远不满足的循环。
\index{infinite loop 无限循环}
\end{description}
\section{练习}
\begin{ex}
\index{algorithm!square root}
测试这章的平方根算法,你可以把结果和{\tt math.sqrt}的结果比较。写一个函数
\verb"test_square_root"打印一个如下的表:
\beforeverb
\begin{verbatim}
1.0 1.0 1.0 0.0
2.0 1.41421356237 1.41421356237 2.22044604925e-16
3.0 1.73205080757 1.73205080757 0.0
4.0 2.0 2.0 0.0
5.0 2.2360679775 2.2360679775 0.0
6.0 2.44948974278 2.44948974278 0.0
7.0 2.64575131106 2.64575131106 0.0
8.0 2.82842712475 2.82842712475 4.4408920985e-16
9.0 3.0 3.0 0.0
\end{verbatim}
\afterverb
第一列是数字$a$,第二列是用\ref{square_root}计算的$a$的平方根,第三列是用
{\tt math.sqrt}计算的平方根,第四列是两个结果的差值。
\end{ex}
\begin{ex}
\index{eval function eval函数}
\index{function!eval}
内置函数{\tt eval}接受一个字符串,然后调用python解释器计算字符串的值,例如:
\beforeverb
\begin{verbatim}
>>> eval('1 + 2 * 3')
7
>>> import math
>>> eval('math.sqrt(5)')
2.2360679774997898
>>> eval('type(math.pi)')
<type 'float'>
\end{verbatim}
\afterverb
编写一个函数\verb"eval_loop"重复的提示用户,接受用户输入,然后调用
{\tt eval}计算值,并打印结果。
程序必须知道用户舒服\verb"'done'"才结束循环,然后返回最后计算的
表达式的值。
\end{ex}
\begin{ex}
\index{Ramanujan, Srinivasa}
杰出的数学家Srinivasa Ramanujan 发现了无穷序列\footnote{参看\url{wikipedia.org/wiki/Pi}.}可以用来产生近似的$pi$值。
\index{pi}
\[\frac{1}{\pi} = \frac{2\sqrt{2}}{9801}
\sum^\infty_{k=0} \frac{(4k)!(1103+26390k)}{(k!)^4 396^{4k}} \]
编写函数\verb"estimate_pi",函数使用上面的公式计算$pi$值,并返回之,
函数应该使用一个{\tt while}循环计算项的和知道最后一项小鱼{\tt le-15}
(Python里的记法为$10^{-15})$。你可以拿它和{\tt math.pi}比较一下。
可以参看我的解答\url{thinkpython.com/code/pi.py}。
\end{ex}