像计算机科学家一样思考:用 Python 学习第二版/迭代
您可能已经发现,对同一个变量进行多次赋值是合法的。新的赋值使现有变量引用新的值(并停止引用旧值)。
该程序的输出为5 7,因为第一次bruce打印时,它的值为 5,第二次打印时,它的值为 7。第一个print语句末尾的逗号抑制了输出后的换行符,这就是为什么两个输出都出现在同一行的原因。
以下是在状态图中多重赋值的样子
多重赋值 在多重赋值中,尤其要注意区分赋值操作和等式。由于 Python 使用等号 (=) 进行赋值,因此很容易将像a = b这样的语句解释为等式。它不是!
首先,等式是对称的,而赋值不是。例如,在数学中,如果 a = 7,那么 7 = a。但在 Python 中,语句a = 7是合法的,而7 = a不是。
此外,在数学中,等式总是成立的。如果现在 a = b,那么 a 将始终等于 b。在 Python 中,赋值语句可以使两个变量相等,但它们不必保持相等
第三行改变了a的值,但没有改变b的值,因此它们不再相等。(在某些编程语言中,使用不同的符号进行赋值,例如<-或:=,以避免混淆。)
多重赋值最常见的形式之一是更新,其中变量的新值取决于旧值。
这意味着获取 x 的当前值,加 1,然后用新值更新 x。
如果您尝试更新一个不存在的变量,您将收到错误,因为 Python 在将结果值分配给左侧的名称之前会评估赋值运算符右侧的表达式
在更新变量之前,您必须初始化它,通常使用简单的赋值
通过加 1 更新变量称为递增;减 1 称为递减。
计算机通常用于自动化重复性任务。重复相同或类似的任务而不犯错误是计算机擅长而人类不擅长的任务。
重复执行一组语句称为迭代。由于迭代非常普遍,Python 提供了几个语言特性来简化此过程。我们将要介绍的第一个特性是while语句。
以下是一个名为countdown的函数,它演示了while语句
的使用。while您几乎可以像读英语一样读语句。它的意思是,当n语句。它的意思是,当大于 0 时,继续显示语句。它的意思是,当的值,然后将的值减 1。当您到达 0 时,显示单词
Blastoff!while语句
- 更正式地说,以下是的执行流程:或评估条件,得出.
- FalsewhileTrue
- 如果条件为假,则退出
语句,并在下一条语句处继续执行。
如果条件为真,则执行主体中的每个语句,然后返回步骤 1。
主体包括标题以下所有具有相同缩进的语句。
这种类型的流程称为循环,因为第三步循环回顶部。请注意,如果条件在第一次循环时为假,则循环内的语句将永远不会执行。countdown循环主体应更改一个或多个变量的值,以便最终条件变为假并终止循环。否则,循环将永远重复,这称为无限循环。计算机科学家们一直以来都津津乐道的一个现象是,洗发水的说明“打泡沫、冲洗、重复”就是一个无限循环。语句。它的意思是,当在语句。它的意思是,当的情况下,我们可以证明循环会终止,因为我们知道语句。它的意思是,当:
的值是有限的,并且我们可以看到的值在每次循环时都会变小,因此最终我们必须到达 0。在其他情况下,并不那么容易判断。请看以下函数,它针对所有正整数定义该循环的条件是语句。它的意思是,当n != 11,因此循环将持续到
等于语句。它的意思是,当为止,这将使条件变为假。语句。它的意思是,当在每次循环中,程序都会输出的值,然后检查它是否为偶数或奇数。如果是偶数,则的值除以 2。如果是奇数,则该值将被替换为
n * 3 + 1语句。它的意思是,当。例如,如果起始值(传递给 sequence 的参数)为 3,则结果序列为 3、10、5、16、8、4、2、1。语句。它的意思是,当由于语句。它的意思是,当有时会增加,有时会减少,因此没有明显的证据证明语句。它的意思是,当会最终达到 1,或者程序会终止。对于某些特定值
,我们可以证明终止。例如,如果起始值是 2 的幂,则语句。它的意思是,当的值将在每次循环中都是偶数,直到它达到 1 为止。前面的示例以这样的序列结尾,从 16 开始。
撇开特定值不谈,有趣的问题是我们是否可以证明该程序对于所有值
都将终止。到目前为止,还没有人能够证明或反驳这一点!跟踪程序
[编辑 | 编辑源代码]为了编写有效的计算机程序,程序员需要培养跟踪计算机程序执行的能力。跟踪涉及成为计算机并跟踪示例程序运行的执行流程,记录程序运行后每个指令执行后所有变量的状态以及程序产生的任何输出。为了理解这个过程,让我们跟踪对语句。它的意思是,当sequence(3)while的调用,该调用来自上一节。在跟踪开始时,我们有一个局部变量,(参数),其初始值为 3。由于 3 不等于 1,因此执行循环主体。打印 3 且的执行流程:3 % 2 == 0被评估。由于它被评估为,因此执行3 * 3 + 1else语句。它的意思是,当.
分支,并评估
n output -- ------ 3 3 10
n * 3 + 1并将结果分配给为了在手动跟踪程序时跟踪所有这些信息,请在一张纸上为程序运行时创建的每个变量添加一个列标题,以及另一个列标题用于输出。到目前为止,我们的跟踪将如下所示评估条件,得出10 != 1被评估为,循环主体再次执行,并打印 10。10 % 2 == 0,因此执行语句。它的意思是,当为真,因此
n output -- ------ 3 3 10 10 5 5 16 16 8 8 4 4 2 2 1
if
变为 5。在跟踪结束时,我们有
[edit | edit source]以下函数计算以十进制表示的正整数中的十进制数字数量。
def num_digits(n):
count = 0
while n != 0:
count = count + 1
n = n / 10
return count
调用num_digits(710)将返回3. 追踪此函数调用的执行过程,以确保它能正常工作。
此函数演示了另一种称为计数器的计算模式。变量count被初始化为 0,然后在每次执行循环体时递增。当循环退出时,count包含结果——循环体执行的总次数,这与数字的数量相同。
如果我们只想计算为 0 或 5 的数字,在递增计数器之前添加一个条件语句即可。
def num_zero_and_five_digits(n):
count = 0
while n != 0:
digit = n % 10
if digit == 0 or digit == 5:
count = count + 1
n = n / 10
return count
确认num_zero_and_five_digits(1055030250)返回 7。
>>> 1055030250 % 10
0
#count = 1
>>> 1055030250/10
105503025
>>> 105503025 % 10
5
#count = 2
>>> 105503025/10
10550302
>>> 10550302 % 10
2
#count = 2 (no change)
>>> 10550302/10
1055030
>>> 1055030 % 10
0
#count = 3
>>> 1055030/10
105503
>>> 105503 % 10
3
#count = 3 (no change)
>>> 105503/10
10550
>>> 10550 % 10
0
#count = 4
>>> 10550/10
1055
>>> 1055 % 10
5
#count = 5
>>> 1055/10
105
>>> 105 % 10
5
#count = 6
>>> 105/10
10
>>> 10 % 10
0
#count = 7
>>> 10/10
1
>>> 1 % 10
1
#count = 7 (no change)
>>> 1/10
0
#n = 0 so loop exits
缩写赋值
[edit | edit source]递增变量非常常见,Python 提供了缩写语法
count += 1是count = count + 1的缩写。递增值不必为 1。
也有-=, *=, /=和%=:
的缩写。
6.7. 表格[edit | edit source]
循环的一个用途是生成表格数据。在计算机普及之前,人们必须手动计算对数、正弦和余弦以及其他数学函数。为了简化这个过程,数学书籍包含了列出这些函数值的冗长表格。创建表格既缓慢又枯燥,并且往往充满了错误。
当计算机出现后,人们最初的反应是:“太棒了!我们可以使用计算机生成表格,这样就不会出现错误了。”结果证明这是正确的(大部分情况下),但目光短浅。此后不久,计算机和计算器变得非常普遍,以至于表格变得过时了。
嗯,几乎如此。对于某些操作,计算机使用数值表来获取近似答案,然后执行计算来改进近似值。在某些情况下,底层表中存在错误,最著名的例子是英特尔奔腾用于执行浮点除法的表。
#program to make tabular data
#x = 1
#prints x (which is 1), then tab, then 2^x (which is 2)
#x += 1, adds 1 to the value of x
#the program runs until the value of x is less than 13, which is 12
x = 1
while x < 13:
print x, '\t', 2**x
x += 1
尽管对数表不再像以前那么有用,但它仍然是迭代的一个很好的例子。以下程序在左列输出一系列值,在右列输出该值 2 的幂。字符串'\t'字符串表示制表符。中的反斜杠字符表示转义序列的开头。转义序列用于表示不可见的字符,例如制表符和换行符。序列\n
表示换行符。
转义序列可以出现在字符串中的任何位置;在本例中,制表符转义序列是字符串中唯一的元素。您认为如何在字符串中表示反斜杠?print当字符和字符串显示在屏幕上时,一个称为光标的不可见标记会跟踪下一个字符将要放置的位置。在
语句之后,光标通常会移到下一行的开头。
1 2 2 4 3 8 4 16 5 32 6 64 7 128 8 256 9 512 10 1024 11 2048 12 4096
制表符会将光标向右移动,直到到达一个制表位。制表符有助于使文本列对齐,如前一个程序的输出所示。
由于列之间存在制表符,因此第二列的位置不依赖于第一列中的数字位数。
6.8. 二维表格[edit | edit source]
二维表格是在行和列的交点处读取值的表格。乘法表就是一个很好的例子。假设您想打印从 1 到 6 的乘法表。
#i = 1
#prints 2 * i (which is 2), then space
#i += 1, adds 1 to the value of i making i = 2
#the program runs until the value of i is equal to or less than 6,
#which is i = 6
#prints 2 * i2 (which is 4)....
#last coma on the print line suppresses the newline
#after loop = complete; second print statement starts newline
i = 1
while i <= 6:
print 2 * i, ' ',
i += 1
print
一个好的开始方法是编写一个循环来打印 2 的倍数,所有倍数都在一行上。第一行初始化了一个名为i第一行初始化了一个名为的变量,它充当计数器或循环变量。随着循环的执行,第一行初始化了一个名为的值从 1 递增到 6。当为 7 时,循环终止。每次循环时,它都会显示2 * i
的值,后跟三个空格。print同样,中的逗号print语句抑制了换行符。循环完成后,第二个
语句开始新的一行。
2 4 6 8 10 12
程序的输出为
到目前为止,一切都很好。下一步是封装和泛化。
6.9 封装和泛化[edit | edit source]封装是将一段代码包装到函数中的过程,使您可以利用函数的所有优点。您已经看到了封装的两个示例:print_parity在第 4 章中;和is_divisible
在第 5 章中。
泛化是指将特定内容(例如打印 2 的倍数)变得更通用,例如打印任何整数的倍数。语句。它的意思是,当:
#i = 1
#say n = 2
#prints n * i (which is 2), then tab
#i += 1, adds 1 to the value of i making i = 2
#the program runs until the value of i is equal to or less than 6,
#which is i = 6
#prints 2 * i2 (which is 4)....
def print_multiples(n):
i = 1
while i <= 6:
print n * i, '\t',
i += 1
print
此函数封装了先前的循环并将其泛化,以打印语句。它的意思是,当.
的倍数。为了封装,我们所要做的就是添加第一行,它声明函数的名称和参数列表。为了泛化,我们所要做的就是将值 2 替换为参数
3 6 9 12 15 18
如果我们使用参数 2 调用此函数,我们将得到与之前相同的输出。使用参数 3,输出为
4 8 12 16 20 24
使用参数 4,输出为现在您可能已经猜到如何打印乘法表——通过重复调用print_multiples
#see above code for description - code included in new program to make it run
def print_multiples(n):
i = 1
while i <=6:
print n * i, '\t',
i += 1
print
#here the value of i goes into above function as n
#and therefore n = 1
#then n * i (1 * 1), '\t'
i = 1
while i <= 6:
print_multiples(i)
i += 1
并使用不同的参数。实际上,我们可以使用另一个循环现在您可能已经猜到如何打印乘法表——通过重复调用注意此循环与中的循环有多么相似print. 我们所做的就是将
语句替换为函数调用。
1 2 3 4 5 6 2 4 6 8 10 12 3 6 9 12 15 18 4 8 12 16 20 24 5 10 15 20 25 30 6 12 18 24 30 36
此程序的输出是一个乘法表
更多封装[edit | edit source]
为了再次演示封装,让我们将上一节中的代码包装到一个函数中
此过程是一个常见的开发计划。我们通过编写任何函数之外的代码行或将它们键入解释器来开发代码。当代码正常工作时,我们会将其提取出来并将其包装到一个函数中。
此开发计划在您开始编写时不知道如何将程序划分为函数时特别有用。这种方法允许您边设计边编写。
局部变量[edit | edit source]第一行初始化了一个名为您可能想知道我们如何在现在您可能已经猜到如何打印乘法表——通过重复调用和print_mult_table中使用相同的变量,
. 当其中一个函数更改变量的值时,不会导致问题吗?第一行初始化了一个名为答案是否定的,因为现在您可能已经猜到如何打印乘法表——通过重复调用中的第一行初始化了一个名为答案是否定的,因为print_mult_table和
中的
不是同一个变量。第一行初始化了一个名为在函数定义内部创建的变量是局部的;您无法从其宿主函数外部访问局部变量。这意味着您可以自由地使用多个具有相同名称的变量,只要它们不在同一个函数中即可。
此程序的堆栈图显示了两个名为第一行初始化了一个名为答案是否定的,因为print_mult_table的变量不是同一个变量。它们可以引用不同的值,更改其中一个不会影响另一个。print_mult_table堆栈 2 图 的值为现在您可能已经猜到如何打印乘法表——通过重复调用从 1 到 6。在图中它恰好是 3。在下一次循环时它将是 4。每次循环时,第一行初始化了一个名为都使用语句。它的意思是,当.
的当前值作为参数调用现在您可能已经猜到如何打印乘法表——通过重复调用. 该值被分配给参数第一行初始化了一个名为在第一行初始化了一个名为答案是否定的,因为print_mult_table.
中,的值从 1 到 6。在图中,它恰好是 2。更改此变量不会影响的值。第一行初始化了一个名为和在不同的局部变量中使用相同的名称是常见的,也是完全合法的。特别是像j
这样的名称经常用作循环变量。如果您只是因为在其他地方使用过它们,而避免在一个函数中使用它们,那么您可能会使程序更难阅读。
更多泛化[edit | edit source]print_mult_table:
我们将值 6 替换为参数high。如果我们调用print_mult_table并使用参数 7,它将显示
1 2 3 4 5 6 2 4 6 8 10 12 3 6 9 12 15 18 4 8 12 16 20 24 5 10 15 20 25 30 6 12 18 24 30 36 7 14 21 28 35 42
这很好,但我们可能希望表格是正方形的——行和列的数量相同。为此,我们向现在您可能已经猜到如何打印乘法表——通过重复调用添加另一个参数以指定表格应该有多少列。
为了烦人,我们将此参数称为high,这表明不同的函数可以拥有相同名称的参数(就像局部变量一样)。以下是整个程序
请注意,当我们添加一个新参数时,我们必须更改函数的第一行(函数标题),并且还必须更改函数在print_mult_table.
中被调用的位置。正如预期的那样,该程序生成一个七乘七的正方形表格。
1 2 3 4 5 6 7 2 4 6 8 10 12 14 3 6 9 12 15 18 21 4 8 12 16 20 24 28 5 10 15 20 25 30 35 6 12 18 24 30 36 42 7 14 21 28 35 42 49
当你适当地概括一个函数时,你经常会得到一个具有你没有计划的功能的程序。例如,你可能会注意到,因为 ab = ba,表格中的所有条目都出现了两次。你可以通过只打印一半的表格来节省墨水。为此,你只需要更改一行print_mult_table。更改
为
,你将得到
1 2 4 3 6 9 4 8 12 16 5 10 15 20 25 6 12 18 24 30 36 7 14 21 28 35 42 49
函数
[edit | edit source]我们已经多次提到了函数的所有优点。到目前为止,你可能想知道这些优点究竟是什么。以下是一些优点:
- 为一系列语句命名使你的程序更容易阅读和调试。
- 将一个长程序分成函数使你能够分离程序的不同部分,独立调试它们,然后将它们组合成一个整体。
- 函数促进了迭代的使用。
- 设计良好的函数通常对许多程序都有用。一旦你编写并调试了一个函数,你就可以重复使用它。
牛顿法
[edit | edit source]循环通常用于计算数值结果的程序,这些程序从一个近似答案开始,并迭代地改进它。
例如,计算平方根的一种方法是牛顿法。假设你想知道语句。它的意思是,当的平方根。如果你从几乎任何近似值开始,你都可以使用以下公式计算出一个更好的近似值:
通过重复应用这个公式,直到更好的近似值等于之前的近似值,我们可以编写一个计算平方根的函数:
尝试使用25作为参数调用此函数,以确认它返回5.0.
算法
[edit | edit source]牛顿法是一个算法的例子:它是一个解决一类问题(在本例中,计算平方根)的机械过程。
很难定义一个算法。从不是算法的东西开始可能会有所帮助。当你学习乘以一位数时,你可能记住了乘法表。实际上,你记住了 100 个特定的解。这种知识不是算法式的。
但是,如果你很懒惰,你可能通过学习一些技巧来作弊。例如,要找到 n 和 9 的乘积,你可以将 n - 1 作为第一个数字,并将 10 - n 作为第二个数字。这个技巧是将任何一位数乘以 9 的通用解。这就是一个算法!
类似地,你学习的进位加法、借位减法和长除法技巧都是算法。算法的一个特点是它们不需要任何智力来执行。它们是机械过程,其中每一步都遵循上一步骤的简单规则集。
在我们看来,人类在学校里花这么多时间学习执行算法是令人尴尬的,因为这些算法从字面上来说不需要任何智力。
另一方面,设计算法的过程很有趣,智力上具有挑战性,并且是我们所说的编程的核心部分。
人们自然而然地做的一些事情,例如没有任何困难或意识思考,是最难用算法表达的。理解自然语言就是一个很好的例子。我们都做到了,但到目前为止,还没有人能够解释我们是如何做到的,至少不是以算法的形式。
术语表
[edit | edit source]练习
[edit | edit source]编写一个单个字符串,该字符串
produces this output.
Â- 在第 6.14 节中定义的sqrt函数中添加一个 print 语句,该语句打印better每次计算时。使用 25 作为参数调用你修改后的函数,并记录结果。
- 跟踪print_mult_table的最后版本的执行,并弄清楚它是如何工作的。
编写一个函数print_triangular_numbers(n),它打印出前 n 个三角形数。调用print_triangular_numbers(5)将产生以下输出:
1 1 2 3 3 6 4 10 5 15
(提示:使用网络搜索找出三角形数是什么。)打开一个名为ch06.py的文件,并添加以下内容:
编写一个函数,is_prime,它接受一个整数参数,当参数为质数时返回评估条件,得出,否则返回的执行流程:。在开发函数时,向函数添加 doctests。什么将num_digits(0)返回?修改它以返回1用于这种情况。为什么调用num_digits(-24)会导致无限循环(提示:-1/10 评估为 -1)?修改num_digits以便它对任何整数值都能正常工作。将以下内容添加到你在上一个练习中创建的文件中:ch06.py将你的函数主体添加到
中,并确认它通过了 doctests。num_digits将以下内容添加到中:ch06.py:
编写num_even_digits的主体,以便它按预期工作。将以下内容添加到ch06.py:
编写print_digits中,以便它通过给定的 doctests。编写一个函数sum_of_squares_of_digits,它计算传递给它的整数的数字平方和。例如,sum_of_squares_of_digits(987)应该返回 194,因为9**2 + 8**2 + 7**2 == 81 + 64 + 49 == 194.
根据上面的 doctests 检查你的解决方案。