在很大程度上,编程是通过计算机解决问题的科学。由于问题通常很困难,因此解决方案以及实施这些解决方案的程序也很困难。为了使您更容易开发这些解决方案,您需要采用一种方法和规程,将复杂程度降低到可管理的规模。
在编程的早期,计算机科学的概念或多或少是一厢情愿的实验。在那个时代,没有人对编程了解太多,很少有人认为它是常规意义上的工程学科。但是,随着编程的成熟,这种学科开始出现。该学科的基石是理解编程是在程序员必须共同工作的社交环境中完成的。如果您进入行业,几乎可以肯定您将成为开发大型程序的众多程序员之一。而且,该程序几乎可以继续使用,并且在其最初预定的应用程序之外还需要main 。有人会希望该程序包含一些新功能或以某种不同的方式工作。发生这种情况时,必须有一个新的程序员团队,并对程序进行必要的更改。如果程序是以个人风格编写的,几乎没有通用性,或者没有通用性,那么要使每个人都富有成效地合作是非常困难的。
为了解决这个问题,程序员开始开发一组统称为的编程方法 软件工程 。使用良好的软件工程技能不仅使其他程序员更容易阅读和理解您的程序,而且还使您更容易编写这些程序。软件工程中最重要的方法学进步之一就是策略 自上而下的设计 要么 逐步细化 ,其中包括从整个问题开始解决问题。您将整个问题分解为多个部分,然后解决每个部分,并在必要时将其进一步分解。这种自上而下的策略得到了补充 迭代测试 在继续之前确保解决方案的较小部分正常工作。
为了说明逐步细化的概念,让我们教Karel解决一个新问题。想象一下卡雷尔现在生活在一个看起来像这样的世界:
在每个列上,都有一个高度未知的锥体 s的塔,尽管有些列(例如示例世界中的第7和第9)可能是空的。卡雷尔(Karel)的工作是收集每个塔中的所有锥体秒,将它们放回第一排的最东角,然后返回其起始位置。因此,当Karel在上述示例中完成工作时,当前塔中的所有25 锥体 s都应堆叠在第9列和第1行的角上,如下所示:
重要的是,您可以假设Karel最初是启动袋子中的零锥体秒。每个拾取的锥体被添加到其包中。将锥体放在角落时,Karel可以使用 锥体_s() 测试。我们还可以假设这些柱子没有一直延伸到最北端的墙。
解决此问题的关键是以正确的方式分解程序,同时仍然可以随需测试。该任务比您所看到的其他任务更为复杂,这使得选择合适的子问题对于获得成功的解决方案更为重要。
逐步完善的关键思想是,您应该从顶部开始设计程序,这是指概念上最高,最抽象的程序级别。在这个级别上, 锥体塔问题显然分为三个独立阶段。首先,Karel必须收集所有锥体秒。其次,卡雷尔必须将它们存放在最后一个十字路口。第三,卡雷尔必须回到原位。这个问题的概念分解表明 main() 该程序的功能将具有以下结构:
def main():
收集所有锥体秒()
删除所有锥体秒()
回家()
在这个级别上,问题很容易理解。当然,还有一些尚未编写的函数形式的细节。即使如此,重要的是要查看分解的每个级别并说服自己,只要您相信将要编写的函数将正确解决子问题,那么您就可以整体解决问题了。
既然您已经定义了整个程序的结构,那么现在是第一个子问题move了,该子问题包括收集所有锥体 。这个任务本身比前面几章中的简单问题更加复杂。收集所有锥体意味着您必须在每个塔中拾取锥体 ,直到到达最后一个角落。您需要为每个塔楼重复操作这一事实表明您需要一个 while 在这里循环。的 while 循环将重复以下过程 收集一个塔() 然后移动。
警告: 尝试编写整个程序是危险的 测试 随你而去如果你弄错了,就很难找到错误。我们知道我们将重复收集一座塔的过程。让我们写和 测试 在我们放入之前收集一个塔 收集一个塔() 在for循环中处理。从而暂时我们可以从以下定义开始 收集所有锥体秒() :
def 收集所有锥体秒() :
#用于测试目的的临时实现
收集一个塔()
move()
作为一个指导原则,如果你有一个复杂的循环,测试身体在编写整个循环之前的循环。
什么时候 收集一个塔() 卡雷尔(Karel)站在锥体 s的锥体或站在一个空的角落。在前一种情况下,您需要在塔中收集锥体 s。在后者中,您可以直接move 。这种情况听起来像是针对 if 语句,您将在其中编写如下内容:
if 锥体():
收集实际的塔()
在将这样的语句添加到代码之前,应考虑是否需要进行此测试。通常,通过观察最初看起来很特殊的情况可以与更一般的情况完全一样地处理,从而使程序变得更加简单。在当前问题中,如果您确定每个大道上都有一个锥体 s的塔,但其中一些塔的高度为锥体 s为零,会锥体什么?利用此洞察力可以简化程序,因为您不再需要测试特定大街上是否有塔楼。
的 收集一个塔() 函数仍然足够复杂,以至于需要进一步分解。为了将所有锥体收集在塔中,Karel需要执行以下步骤:
再次,此大纲为 收集一个塔() 函数,如下所示:
def收集一个塔():
turn_left()
收集锥体秒()
turn_around()
move至墙()
turn_left()
的 turn_left() 命令的开头和结尾 收集一个塔() 功能对于该程序的正确性均至关重要。什么时候 收集一个塔() 据说,卡雷尔(Karel)总是在第一排朝东的某个地方。完成操作后,仅当Karel在同一拐角处再次面向东方时,整个程序才能正常运行。在调用函数之前必须为真的条件称为 先决条件 ;功能完成后必须适用的条件称为 后置条件 。
定义函数时,如果准确记下前置条件和后置条件是什么,麻烦将大大减少。完成此操作后,您需要确保所编写的代码始终满足后置条件,前提是前提是先满足先决条件。例如,考虑一下如果您致电 收集一个塔() 当卡雷尔(Karel)在第一排朝东时。首先 turn_left() 函数使Karel朝北,这意味着Karel与代表塔的锥体列正确对齐。的 收集锥体秒() 函数-尚未编写,但是执行的是概念上您了解的任务-只需move s即可旋转。因此,在通话结束时, 收集锥体秒() ,Karel仍将朝北。的 turn_around() 因此,电话让Karel朝南。喜欢 收集锥体秒() , move至墙() 函数不涉及任何转弯,而仅涉及move s,直到撞到边界墙为止。由于卡雷尔(Karel)朝南,因此该边界墙将位于屏幕底部,即第一行的下方。决赛 turn_left() 因此,司令部将卡雷尔(Karel)放在面向东方的第一排,满足了后置条件。
您使用run程序,它成功清除了一个塔并使Karel处于承诺的后置条件中。哇!您刚刚完成了这项艰巨任务的里程碑!我们现在必须重复使用以下步骤清理一个塔的过程 while 循环。
但是这是什么 while 循环是什么样的?首先,您应该考虑条件测试。您希望Karel在行尾碰到墙时停下来。因此,您希望Karel持续到front is clear的空间。因此,您知道 收集所有锥体秒() 功能将包括 while 循环使用 front_is_clear() 测试。在每个位置,您希望Karel收集塔中从该角开始的所有锥体 s。如果给该操作起一个名字,可能类似于 收集一个塔() ,您可以继续为 收集所有锥体秒() 即使您尚未填写详细信息,也可以使用该功能。
但是,您必须小心。的代码 收集所有锥体秒() 看起来不是这样的:
def收集所有锥体秒():
#越野车!
while front_is_clear():
收集一个塔()
move()
此实现存在错误,其原因与通用版的第一个版本完全相同 锥体线 第6章中的程序无法完成其工作。此版本的代码中存在锥体错误,因为Karel需要测试最后一条大道上是否存在锥体塔。正确的实现是:
def收集所有锥体秒():
while frontIsClear():
收集一个塔()
move()
收集一个塔()
请注意,此函数的结构与第6章介绍的Place金字塔线程序中的main程序完全相同。唯一的区别是该程序调用 收集一个塔() 另一个叫 放锥体() 。这两个程序分别是通用策略的示例,如下所示:
def收集所有锥体秒():
while front_is_clear():
执行一些操作。
move()
对最后一个角落执行相同的操作。
每当您需要在move处move的路径上的每个角落执行操作时,都可以使用此策略。如果您记得此策略的一般结构,则在遇到需要这种操作的问题时可以使用它。这种可重用策略在编程中经常出现,被称为 编程习语 要么 模式 。您知道的模式越多,您就越容易找到适合特定类型问题的模式。
尽管已经完成了艰苦的工作,但是仍然有一些松散的末端需要解决。 main程序调用两个函数- 删除所有锥体秒() 和 回家() -尚未成文。同样, 收集一个塔() 来电 收集锥体秒() 和 move至墙() 。幸运的是,所有这四个函数都很简单,无需进一步分解即可进行编码,特别是如果您使用 move至墙() 在...的定义中 回家() 。这是完整的实现: