6D: 设计、排除错误和“甜甜圈”

本课有一些建议,关于如何使设计和调试你的程序变得更容易。你做编程的时间越长,在避免和修复错误上就会变得更好。但在这里,我们会尝试给你一些非常常用及有用的建议。我们会讲解以下技巧:

  • 一次建立一个部分
  • 动手来解决一些例子
  • 在你开始写代码之前先计划好你的代码
  • 编写代码
  • 动手测试你解决的例子
  • 测试额外的例子包括 “边界”和一些随机的情况

这个神奇的公式不会自动解决一切,但它可以节省你的时间,并减少你处理后的问题的数量。

当你遇到一个问题,一些帮助调试它的策略有 使用诊断打印语句 (我们将在以后的课中使用), 使用可视化工具, 仔细阅读代码, 以及编写好的测试。 如果你 在家中运行Python, 你可以使用断点和分步的工具来更好的理解错误。

一个 运算法则 意味着一系列的指令。 而计算机程序需要使用特定的语言编写,如Python和JavaScript。algorithm这个词意味着一步一步的指示,可以用语言或图表示。例如,一个甜甜圈食谱是一个算法,其中“量出3杯面粉”一步,“让面团发酵两小时”是另一步。我们会在这节课中开发一个简单的算法。 然后我们会落实这意味着将它转换成一个可以运行的计算机程序。

问题:甜甜圈计算器

Tim Horton's, 一家在加拿大的咖啡连锁店, 销售 timbits, 一种好吃的甜甜圈球:

Photo: looking_and_learning, flickr

你必须在接下来的几周里组织几次聚会,你会对每一个聚会订购不同数量的timbits。所以为了你的预算计划,你想做一个程序来计算任意数量timbits的价格。在你当地的分店,都是下面这些价格:

数量 价格
1 $0.20
10 (小盒) $1.99
20 (中盒) $3.39
40 (大盒) $6.19

一次建立一个部分

最终,你想要编写一个程序,它的输入是 在聚会上的人数——P,它的输出是总共的含税支出。在最高的层面上,这个程序有三个部分:

  1. 计算当有P个人时,你一共需要买多少(T)个甜甜圈球?
  2. 计算买T个甜甜圈球的价格
  3. 计算税和总共的花销。

事实上,你可以一个接一个的完成这三个部分。同样,我们也建议你如此进行。在你每进入下一步骤之前,记得测试和修改你已完成的部分,因为越大的程序,越难发现和解决每个错误。在组合成一个完整的程序之前,三个部分分开进行会让你更容易开发和测试。对这节课的其余部分,我们会将重点放在步骤2:计算购买T个timbits的价格。

动手来解决一些例子

为了能够告诉电脑做某件事,你首先需要能够自己完成它。让我们通过一个例子来练习一下:购买 45 个timbits的价格是多少? 让我们看下表格,我们可以买大盒装(40个),需要$6.19。这让我们还需要单独买 45 – 40 = 5 个timbits, 它们需要额外的 5 * $0.20 = $1.00。总价会是 $6.19 + $1.00 = $7.19.

在你开始写代码之前先计划好你的代码

如果我们想买4个或456个 timbits而不是45个呢?总的来说,我们应该买尽可能多的大盒装,然后再换成中盒装或小盒装,最后才是购买单独的 timbits 来得到正确的总数。

这种算法真的是最优的吗?是的! 你可以看到,买两个中盒装(2*$3.39 = $6.78) 是一个坏主意,因为我们可以更便宜的在一个大盒装中买到 2*20=40 timbits。同样的比较可以在单个与小盒、小盒与中盒之间进行,这些比较可以用来证明这个算法是最优的。 如果价格或者大小不一样,我们可能需要更小心的考虑。

我们之前说过,你应该在开始编写代码前,做好计划。你可以:

  • 用中文写出一个简短的算法分步描述
  • 使用图表或者流程图来显示所有的步骤
  • 当下,你应该把重点放在整体框架上,而不是细节。

在我们的例子中, 我们会给出一个文字描述。 我们没有给你显示这个特定程序的流程图,因为它只是从步骤1到步骤2之间的直线流动。带有循环的程序,就像我们随后会看到的,会从流程图中获得更多的效益。

计算出任意数量的timbits价格的算法

  1. 获取timbits输入的数量。
  2. 从零开始,跟踪总价格。
  3. 尽可能的购买大盒装。
    1. 计算还需要多少timbits。
    2. 更新总价格。
  4. 如果可以,购买一个中盒装,并重复步骤A和B。
  5. 如果可以,购买一个小盒装,并重复步骤A和B。
  6. 购买单独的timbits并重复步骤B。
  7. 输出总价

我们把这叫做 伪代码,因为它是一个串联的一步一步的说明,但却没有使用任何真正的编程语言。

编写代码

重申我们的第一条建议, 每次只建立一个部分, 你也可以考虑在从1到7的每一步中写一个单独的代码块。此外,这里还有一件事,我们需要考虑所有的步骤:我们要用什么样的变量?他们的名字,类型和初始值是什么?

让我们用 timbitsLeft 来代表我们还需要购买timbits的数量,同时我们用totalCost来代表目前为止的总价。我们不需要事先告诉Python他们的类型,但我们可以先计划好: that timbitsLeft 将会是 int类型, 同时 totalCost 会是 float类型。 (所以一分钱是 0.01)。在每一步中,我们还需要一些额外的临时变量,它们只有在我们需要时才会使用到。

第一步和第二部可以各用一行代码来完成:

timbitsLeft = int(input()) # 步骤1: 获取输入
totalCost = 0              # 步骤2: 初始化总价格
在步骤3中, 我们需要计算最多可以买多少个大盒装。我们怎样才能计算它呢?因为每一个大盒装有40个timbits,数字bigBoxes 应该是 timbitsLeft 除以 40的整数商。 持续记录这些运算成分,使我们有了下面这段代码:

# 步骤3: 尽可能的购买大盒装
bigBoxes = timbitsLeft / 40
totalCost = totalCost + bigBoxes * 6.19    # 更新总价
timbitsLeft = timbitsLeft - 40 * bigBoxes  # 计算剩余还需要的timbits数量
事实上,我们在这已经犯了一个错误。 捕获错误的最好方法是尽早测试你的代码,并经常测试你的代码!让我们乐观地说,我们现在已经测试了部分代码。我们只需要添加一些打印语句来观察它目前的行为。(除了使用打印语句,你也可以使用我们的可视化工具来检查每一步。)

示例
使用输入 45 来测试目前的程序,并使用print 语句来帮助你排除故障。

这并不是我们想要的结果!问题在哪里? 哪一行代码使我们的程序发生了改变? 试着在打开下一段之前找到它。记着,我们在前面自己动手完成了这个问题。

点击来查阅解释
罪魁祸首是这行代码 bigBoxes = timbitsLeft / 40, 因为 bigBoxes 出来的结果是 1.125,而我们想要它等于 1。你不可能买一盒的一小部分。一个可行的解决方法(你会在课程7A中看到),就是在整数除法中使用 // 来替代十进制除法的/。另一个基于我们在第4课中看到的方法是转换 timbitsLeft / 40 为一个 int,这将删除这个数的小数部分。我们将使用这种方法:于是这行代码变成了 bigBoxes = int(timbitsLeft / 40)

步骤4和5与步骤3十分相似。另外,因为你会买最多一个中型盒子或一个小盒子,我们可以使用一个if 语句:

if timbitsLeft >= 20: # 步骤4,我们能买一个中盒装么?
  totalCost = totalCost + 3.39
  timbitsLeft = timbitsLeft - 20
if timbitsLeft >= 10: # 步骤5, 我们可以买一个小盒装么?
  totalCost = totalCost + 1.99
  timbitsLeft = timbitsLeft - 20
最后,我们需要付20分给每个额外单独购买的timbits并输出最终答案。

totalCost = totalCost + timbitsLeft * 20 # 步骤6
print(totalCost)                         # 步骤7

测试你完成的例子

这里是整体的程序。我们会做的第一件事是测试当输入为45 时,它是否正确运行。

示例
45个timbits的价格是多少?
你可以在下面的框中输入程序的输入。

这里肯定存在一个错误,因为这45个timbits竟然需要100多元!你的最后一份工作就是找到并消除这个错误,并修复隐藏在代码中的另一个错误。它将在下一个部分中被测试。

这里有一个关于Tim Horton的价格的小趣事:有可能你买 T+1个 timbits会比买 T 个timbits更便宜。例如,购买19个timbits需要$3.79, 但购买20个timbits却只需要$3.39。不管怎样, 我们目前只关心怎样最便宜的购买正好 T个timbits, 因此之前的算法是完全正确的。所以你的程序应该输出3.79 当输入是 19时。

测试额外的例子

在计算机科学圈里,我们尝试智能自动化的测试你的代码,以确保你已经正确地解决了这个问题。但在一个真正的编程环境中,应该由你来彻底测试自己的代码。这里有一些有用的测试指南。

  • 你可以检查你程序的 每一行代码 来确认这个程序是正常工作的。在timbit 这个问题中,使用输入 10 可以帮助检查我们是否正确的处理了小盒的情况。同样的,输入 2040 可以帮我们检查中盒和大盒的情况。最后,你应该检查单个timbit的情况(例如使用输入1)。
    • 这个可以帮助我们检查像$3.39和$1.99这样的值是否键入正确。
  • 记得检查会超出你程序限制的 "边界线" 情况。例如,这个程序可能的最小输入是 0, 因为你不可能买负数个timbits。我们的程序并没有最大输入值,但你可以尝试一些值,例如3941,他们都非常接近刚够买一大盒的界限。
    • 这个可以帮助检查你是否不小心打入了> 当你真正需要打入的是 >=
  • 在一些情况下你可以自动检查每一个 输入,或者检查很多随机的输入来看你的程序是否正常工作。在此网站的内部测试中,我们使用了大量的随机输入。使用 import random 在你程序的开头,接着例如你使用代码random.randint(10, 100),它就会产生一个在10100之间的随机整数。

你能够找到所有的错误并通过所有的测试吗? 尝试一下吧!

在Python中,一些计算会产生一些小的错误:
示例: Rounding Error
令人惊讶的是,上面这行代码并不会给我们整好0.35!这是由于Python存储信息的方式造成的。你不用担心这些极小的错误:打分软件会自动无视他们。我们会在课程7B中更详细的说明这件事。

编程练习: Timbits
请尝试修复所有bug并且通过所有测试!

饿了吗?你已经准备好进入下节课了!