Think Python/迭代
正如你可能已经发现,对同一个变量进行多次赋值是合法的。新的赋值会使现有变量引用新值(并停止引用旧值)。
bruce = 5
print bruce,
bruce = 7
print bruce
该程序的输出为5 7,因为第一次bruce被打印时,其值为 5,第二次打印时,其值为 7。第一个print语句末尾的逗号抑制了换行符,因此两个输出都出现在同一行上。
以下是多重赋值在状态图中的样子
对于多重赋值,区分赋值操作和等式语句尤为重要。由于 Python 使用等号 (=) 进行赋值,因此很容易将像a = b这样的语句解释为等式语句。它不是!
首先,等式是一个对称关系,而赋值不是。例如,在数学中,如果 a = 7,那么 7 = a。但在 Python 中,语句a = 7是合法的,而7 = a则不是。
此外,在数学中,等式语句始终为真或假。如果 a = b,那么 a 将始终等于 b。在 Python 中,赋值语句可以使两个变量相等,但它们不必保持这种状态
a = 5
b = a # a and b are now equal
a = 3 # a and b are no longer equal
第三行更改了a的值,但没有更改b的值,因此它们不再相等。
虽然多重赋值通常很有帮助,但应谨慎使用。如果变量的值频繁更改,它会使代码难以阅读和调试。
多重赋值最常见的形式之一是更新,其中变量的新值取决于旧值。
x = x+1
这意味着“获取x的当前值,加 1,然后用新值更新x。”
如果你尝试更新一个不存在的变量,你会收到一个错误,因为 Python 在为x:
>>> x = x+1
NameError: name 'x' is not defined
赋值之前先评估右侧。
>>> x = 0
>>> x = x+1
在更新变量之前,你必须初始化它,通常使用简单的赋值
计算机通常用于自动化重复性任务。重复相同或相似的任务而不会出错是计算机擅长的事情,而人类则不擅长。
我们已经看到了两个程序,countdown和 print_n
,它们使用递归来执行重复,也称为迭代。由于迭代非常常见,因此 Python 提供了几个语言特性来简化操作。其中之一是我们在第 4.2 节中看到的for语句。我们稍后会回到它。
另一个是while语句。以下是用countdown语句编写的while语句
def countdown(n):
while n > 0:
print n
n = n-1
print 'Blastoff!'
版本。while你几乎可以像读英语一样阅读语句。它的意思是,“当n语句。它的意思是,“当大于 0 时,显示语句。它的意思是,“当的值,然后将的值减 1。当你到达 0 时,显示文字”
更正式地说,以下是while语句
- 语句的执行流程:评估条件,得到True或.
- Falsewhile如果条件为假,则退出
- 语句,并继续执行下一条语句。
如果条件为真,则执行主体,然后返回步骤 1。
这种类型的流程称为循环,因为第三步会循环回到顶部。
在countdown的情况下,我们可以证明循环会终止,因为我们知道语句。它的意思是,“当的值是有限的,并且我们可以看到语句。它的意思是,“当的值在每次循环中都变小,因此最终我们必须得到 0。在其他情况下,要判断是否会终止并不容易
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
该循环的条件是n != 1,因此循环会继续,直到语句。它的意思是,“当等于1,这使得条件为假。
每次循环时,程序都会输出语句。它的意思是,“当的值,然后检查它是偶数还是奇数。如果它是偶数,语句。它的意思是,“当除以 2。如果它是奇数,语句。它的意思是,“当的值将被替换为n*3+1。例如,如果传递给sequence的参数是 3,则生成的序列为 3, 10, 5, 16, 8, 4, 2, 1。
由于语句。它的意思是,“当有时增加有时减少,因此没有明显的证据证明语句。它的意思是,“当最终会达到 1,或者程序会终止。对于语句。它的意思是,“当的某些特定值,我们可以证明终止。例如,如果起始值为 2 的幂,那么语句。它的意思是,“当的值在每次循环中都将为偶数,直到它达到 1。前面的示例以这样一个序列结束,从 16 开始。
难题在于我们是否可以证明该程序对于所有正值语句。它的意思是,“当都将终止。到目前为止1,还没有人能够证明或反驳它!
用迭代而不是递归重写第 '5.8' 节中的函数 print_n
。
有时你不知道什么时候结束循环,直到你执行到循环主体的一半。在这种情况下,你可以使用break语句跳出循环。
例如,假设你想要从用户那里获取输入,直到他们输入done。你可以编写
while True:
line = raw_input('> ')
if line == 'done':
break
print line
print 'Done!'
循环条件是评估条件,得到,它始终为真,因此循环会一直运行,直到遇到 break 语句。
每次循环时,它都会用一个尖括号提示用户。如果用户输入done,则break语句将退出循环。否则,程序会回显用户输入的任何内容,然后返回循环顶部。以下是一个示例运行
> not done
not done
> done
Done!
这种编写while循环的方法很常见,因为你可以在循环中的任何地方检查条件(而不仅仅是在顶部),并且你可以用肯定的方式表达停止条件(“当发生这种情况时停止”),而不是否定方式(“一直持续到发生这种情况”)。
循环经常用于计算数值结果的程序,这些程序从一个近似答案开始,然后迭代地对其进行改进。
例如,计算平方根的一种方法是牛顿法。假设你想要知道 a 的平方根。如果你从几乎任何估计值 x 开始,你可以使用以下公式计算一个更好的估计值
y = |
|
例如,如果 a 为 4,x 为 3
>>> a = 4.0
>>> x = 3.0
>>> y = (x + a/x) / 2
>>> print y
2.16666666667
这更接近正确答案 (√4 = 2)。如果我们用新的估计值重复这个过程,它会更接近
>>> x = y
>>> y = (x + a/x) / 2
>>> print y
2.00641025641
再经过几次更新后,估计值几乎完全正确
>>> x = y
>>> y = (x + a/x) / 2
>>> print y
2.00001024003
>>> x = y
>>> y = (x + a/x) / 2
>>> print y
2.00000000003
一般来说,我们事先不知道需要多少步才能得到正确答案,但我们知道什么时候得到答案,因为估计值停止变化
>>> x = y
>>> y = (x + a/x) / 2
>>> print y
2.0
>>> x = y
>>> y = (x + a/x) / 2
>>> print y
2.0
当y == x时,我们可以停止。以下是一个循环,它从一个初始估计值x开始,并对其进行改进,直到它停止变化
while True:
print x
y = (x + a/x) / 2
if y == x:
break
x = y
对于大多数a的值,这都可以正常工作,但一般来说,测试float的相等性是危险的。浮点数只是近似正确的:大多数有理数,如 1/3,以及无理数,如 √2,不能用float.
精确地表示。与其检查x和y如果两个数字完全相等,使用内置函数abs计算它们之间的差的绝对值或幅度更安全。
if abs(y-x) < epsilon:
break
其中epsilon
的值为0.0000001这决定了多接近才足够接近。
将此循环封装到一个名为square_root
的函数中,该函数以 'a' 为参数,选择一个合理的 'x' 值,并返回 'a' 平方根的估计值。
牛顿法是算法的一个例子:它是一个用于解决一类问题(在本例中是计算平方根)的机械过程。
定义算法并不容易。从非算法开始可能会有所帮助。当你学习乘以一位数时,你可能记住了乘法表。实际上,你记住了 100 个特定的解决方案。这种知识不是算法。
但是,如果你很“懒惰”,你可能通过学习一些技巧来作弊。例如,要找到n和 9 的乘积,你可以将n−1 写作第一位,将 10−n 写作第二位。这个技巧是将任何一位数乘以 9 的通用解决方案。这就是算法!
同样地,你所学的带进位加法、带借位减法和长除法技巧都是算法。算法的一个特点是它们不需要任何智能来执行。它们是机械过程,其中每一步都遵循最后一步的简单规则。
在我看来,人类在学校花费大量时间学习执行算法,这真是令人尴尬,因为这些算法实际上不需要任何智能。
另一方面,设计算法的过程很有趣,具有智力挑战性,并且是我们所称的编程的核心部分。
人们自然而然地做的一些事情,没有任何困难或意识思考,是最难用算法表达的。理解自然语言就是一个很好的例子。我们都会做,但到目前为止,还没有人能够解释我们是如何做到的,至少不是以算法的形式。
当你开始编写更大的程序时,你可能会发现自己花费更多时间调试。更多的代码意味着更多的出错机会,也意味着更多的地方隐藏错误。
减少调试时间的一种方法是“二分调试”。例如,如果你的程序中有 100 行代码,而你逐行检查它们,则需要 100 步。
相反,尝试将问题分成两半。查看程序的中间部分或附近部分,以检查中间值。添加一个print语句(或其他具有可验证效果的内容)并运行程序。
如果中间点检查不正确,则问题必须在程序的前半部分。如果正确,则问题在后半部分。
每次执行这样的检查时,你都会将要搜索的代码行数减半。在六步之后(这远小于 100 步),至少在理论上,你会减少到一两行代码。
在实践中,并不总是清楚“程序的中间部分”是什么,也不总是可以检查它。计算行数并找到确切的中间点没有意义。相反,思考程序中可能存在错误的位置以及易于放置检查的位置。然后选择一个你认为错误在检查之前或之后的概率大致相同的位置。
- 多重赋值
- 在程序执行期间对同一个变量进行多次赋值。
- 更新
- 变量的新值取决于旧值的赋值。
- 初始化
- 对将要更新的变量赋予初始值的赋值。
- 递增
- 增加变量值的更新(通常增加 1)。
- 递减
- 减少变量值的更新。
- 迭代
- 使用递归函数调用或循环重复执行一组语句。
- 无限循环
- 循环中的终止条件永远不会满足。
为了测试本章中的平方根算法,你可以将其与 'math.sqrt' 进行比较。编写一个名为test_square_root
的函数,该函数打印如下表:
''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
''
第一列是一个数字 'a';第二列是使用练习 '7.2' 中的函数计算的 'a' 的平方根;第三列是 'math.sqrt' 计算的平方根;第四列是两个估计值之间差的绝对值。
内置函数 'eval' 接受一个字符串并使用 Python 解释器对其进行求值。例如
''>>> eval('1 + 2 * 3')
7
>>> import math
>>> eval('math.sqrt(5)')
2.2360679774997898
>>> eval('type(math.pi)')
<type 'float'>
''
编写一个名为eval_loop
的函数,该函数以交互方式提示用户,获取结果输入并使用 'eval' 对其进行求值,并打印结果。
它应该继续直到用户输入done
,然后返回它最后求值的表达式的值。
天才数学家斯里尼瓦萨·拉马努金发现了一个无穷级数2,可用于生成 的数值近似值:
编写一个名为estimate_pi
的函数,该函数使用此公式来计算并返回 'π' 的估计值。它应该使用 'while' 循环来计算求和的项,直到最后一项小于 '1e-15'(这是 Python 表示 10−15 的符号)。你可以通过将其与 'math.pi' 进行比较来检查结果。
你可以在 'thinkpython.com/code/pi.py' 上查看我的解决方案。
- 1
- 参见wikipedia.org/wiki/Collatz_conjecture.
- 2
- 参见wikipedia.org/wiki/Pi.