本节书摘来自华章计算机《从问题到程序:用Python学编程和计算》一书中的第1章,第1.3节,作者:裘宗燕 更多章节内容可以访问云栖社区“华章计算机”公众号查看。
1.3 程序开发
在用Python学习编程时,自然需要了解Python语言,但更重要的是学习、理解和运用人们长期程序设计工作总结出的经验,包括正确的思考问题方法、正确的程序开发方法以及一些有益的常规做法,还要养成良好的编程习惯。随着学习的深入,需要解决的问题也会变得越来越复杂(当然,实际中的问题和解决它们的程序更复杂得多)。比较复杂的东西不是随随便便就能做好的,需要认真工作,也需要正确的工作方法。本书中许多地方提出了这些方面的建议,希望引起读者的重视。
本节简单讨论程序的开发过程,包括程序的设计、实现(编程)、测试(testing)和排除错误(简称排误,做实际工作的人们通常将其称为调试,英文词是debugging)等方面的问题,这些都是程序开发中必经的工作阶段。对初学者而言,因为缺乏程序设计实践,下面讲到的一些情况,初读起来可能无法完全理解,但是这些问题确实应该说明,因此放在这里集中讨论。希望读者在学习了本书中的若干章节,做了一些程序后再回来重读这些说明,有可能更好地理解这里的讨论及其重要意义。
1.3.1 程序开发过程
用计算机和Python语言解决问题的过程如图1.6所示,其中包含一系列的步骤,用矩形块表示,开发过程的主线用宽箭头表示,其他实线和虚线箭头表示在一些情况下的工作转移,说明在这种工作过程中可能有反复。下面是一些说明:
1)分析,严格化:需要用计算机解决的问题来自实际,即使是一道习题,一般也是用自然语言说明的。编程工作的第一步是把问题弄清楚,确定到底需要做什么。
2)设计:根据问题的清晰描述,设法找到一种解决问题的途径(解决方案)。
3)编程:采用某种编程语言(本书中用的是Python)写出解决问题的程序(代码)。我们可以用普通文本编辑器或者专门的程序开发环境编写程序。在下面讨论Python编程时,考虑的是用Python系统自带的编程环境IDLE。
https://yqfile.alicdn.com/1b2f029163349c3d785fa9b8fca098e65a7c8fad.png" >
4)检查(检查源程序):人工检查和/或用语言系统检查。发现错误时需要设法确定错误根源,然后予以更正。实际上,编程和人工检查经常交替进行或同时进行。开发出一段Python程序后,人们就会把它送给解释器检查。此外,检查中发现的错误也不一定是简单的编程错误,有可能是前面的分析或者设计有错,发现这种情况时就需要回到前面阶段。通过了Python解释器检查的程序就可以运行了。
5)测试/调试:以适当方式运行程序,送给它适当的数据,让它工作。通过这种试验运行检查程序的工作情况是否正常,产生的效果或者结果是否满足需要。发现错误后应设法确定错误根源,回到前面步骤修改设计或程序等。
重复进行上述过程,有时可能要回到问题分析、方法设计步骤。经过反复测试,直到确信程序正确为止。下一小节将专门讨论与程序错误有关的情况。
1.3.2 程序错误
人很容易犯错误,做复杂的事情时经常出错。做一道复杂数学题、写一篇长文时,很难保证其中没有写错的文字或描述。程序可能很复杂,开发程序的过程可能很长(参见图1.6)。另一方面,写出的程序是一段静态的符号文本,其意义要通过在计算机上执行而体现。程序的静态文本与动态执行之间的关系并不容易把握,编程学习的一项重要内容就是学习理解程序的意义。综合上面各方面的因素,归结到一句话:编写程序的过程中出错的情况很常见,因此,纠错是编程中不可避免的一项重要活动。
有关排除程序错误的术语是Debugging,关于它还有一个传说:在计算机发展早期的某一天,一台计算机出了故障。人们仔细检查,发现计算机里有一个被电流烧焦的小虫(bug),由此造成电路短路,小虫是这次故障的祸根。此后检查排除计算机故障的工作就被称为Debugging,也就是“找虫子”。后来人们也这样称呼检查程序错误的工作。
实际上,对于编程而言,这个词并不贴切。在程序里出现的错误都是编程序者自己在工作中犯的错误,没有其他客观原因,也没有虫子之类的东西在捣乱。开始学习程序设计就应该牢记这个情况:所谓排除程序里的错误,也就是排除自己在编程中犯下的错误,排除自己写在程序里的错误。初学者遇到自己的程序出错时,常倾向于认为是系统或计算机有问题,常常会说“我的程序绝对没错,一定是……”。而有经验的程序员都知道,如果自己的程序出了错,基本上可以肯定是自己的错,自己的责任。
程序错误可以分为两大类,一类是程序的书写形式在某些方面不符合语言要求。语言系统在处理程序时可以检查出这类错误。另一类是程序的书写形式没错,可以正常执行。但是可能在执行中报告运行错误;或者程序的执行能正常完成,但产生的结果(或执行效果)不符合需要。所谓排除程序的错误,就是要消除这两类错误。
下面讨论特别针对用CPython和IDLE开发Python程序的情况。使用其他工具开发Python程序的情况类似,有些工具在编辑程序时就能及时发现一些错误。但无论如何,可能出现错误的情况和如何检查排除的道理方面没什么不同。
检查程序可能发现的错误
IDLE编辑器的Run菜单之下有一个Check Module命令,用于检查正在编辑的程序。此外,要求Python运行程序时,解释器也会先检查这个程序。检查过程中可能发现一些错误,这时解释器的处理立刻停止,产生一些出错信息,并在源程序中标出发现错误的位置。遇到这种情况,我们就应该仔细阅读这些错误信息,检查解释器指定的位置附近的源程序代码,找到真正错误原因并予以排除,然后再继续工作下去。
Python解释器在检查中能发现的错误主要有两类:
1)语法错误(错误名为SyntaxError):即程序中某些部分的内容或结构不符合Python语言的基本语法要求。错误的原因如缺少必要的符号(常见的如缺少冒号),几个字符构成的关键组合符号拼写不正确,使用的名字不符合Python基本要求等。
2)程序格式错误(IndentationError,TabError):Python对程序的编排有特殊要求,如一些代码行需要相互对齐,不能交替使用空格和制表符(制表符用键盘Tab键输入)。在程序格式方面,IDLE和其他专门支持Python程序开发的软件开发环境都能提供很多帮助,它们能自动处理Python程序中的格式。如果用普通的文本编辑器写Python程序,或者自己在程序里随便加空格,就可能造成这类错误。
解释器的工作方式是按照程序文本的描述,一个个字符地顺序检查Python程序,如果检查到某一点确定了程序有错,就把这一点标记为发现错误的位置。源程序里的实际错误有可能出现在解释器标出的位置,也可能出现在这个位置之前。看到解释器的错误报告后,我们应当从指明的位置开始向前检查,设法确定错误原因。有些错误可能在实际出现以后很久才被解释器发现,也就是说,实际错误可能出现在解释器所指位置之前很远的地方。一般而言,这类错误都比较容易确认和更正。
程序运行中发现的错误
一旦程序通过了检查,我们就可以用Python解释器去运行它。在Python程序的运行中可能出现几类不同的错误,需要设法解决:
1)程序的运行突然停止,解释器报告运行中出错。这种错误称为动态运行错误,说明运行中出现非正常情况,导致某些操作无法完成。算术运算中遇到除数为0的情况就是一例。可能导致程序以非正常方式终止的错误很多,后面章节中有些介绍。
2)程序执行中不报告错误,但也一直不结束,或是长时间没有任何反应,或是反复地输出一些类似信息。这些现象说明程序执行有可能进入了死循环,也就是说,永无休止地重复执行一段代码。当然,长时间无反应未必说明程序进入死循环。如果一个程序要求输入,它也会进入等待状态,直到人输入信息后才继续。另一方面,有的程序确实需要运行很长时间。程序是否真正进入死循环,还需要仔细分析和判断。在IDLE里,可以通过Ctrl-C组合功能键强制结束当前正在执行的程序。
3)程序能执行到结束,但得到的结果不正确,或程序执行中产生的效果不符合需要。这说明程序有语义错误,或称逻辑错误,说明程序编错了。这种错误的根源更复杂,可能是做问题分析工作时没有看清情况,也可能是算法设计不对,或者是编程中有意无意引进的错误,例如写错了变量名字等。这类错误是最难确认更正的,下面有些讨论,后面章节里还有进一步的说明。
当然,Python系统和计算机上的操作系统也是程序,虽然经过仔细测试和长期使用,也不能保证其内部绝对没有错误。如果在运行一个Python程序时解释器突然崩溃,或者整个计算机死机,那就有可能是Python系统或计算机操作系统的问题,需要找更专业的人士来解决。但这类情况极罕见。一般而言,程序运行中出问题,都是我们自己的程序有错,需要设法排除错误,这是程序开发过程中的一项重要工作。
排除程序错误
解释器检查阶段可能发现的错误通常比较简单,容易找到根源并更正。本小节主要关注如何解决在程序的试验运行中发现的错误。显然,这时的工作就是设法弄清错误原因,找到实际包含错误的代码并更正之。
发现错误后,首先应该人工地做一些分析工作:弄清错误的表现,根据发现的情况分析造成错误的各种可能,仔细分析输入数据和得到的结果,分析可能出错的程序片段,仔细阅读这些片段,分析其可能行为,逐步排除疑点。通过这种人工工作,在很多情况下都能最终定位有错误的程序代码,下面的工作就是设法纠正错误。如果确认出错的只是程序代码,就应该仔细考虑如何修改代码,排除错误。如果最后发现问题的根源是在编程之前的工作步骤,就需要转回到那里去设法解决问题。
即使直接的人工检查不能发现错误,也常常会发现程序中的一些疑点。在这种情况下,就应该选择一些特殊的数据做进一步试验,设法确认自己的认识,缩小错误范围。进而设法找到导致错误的最简单数据。修改出错的程序后再重新试验运行。经过一系列试验和仔细分析,程序中较为简单的错误都可能得到确认。
如果直接分析各种可见的现象后仍不能确定错误的原因,就需要采用各种动态检查技术。动态检查的基本方法是检查程序执行的过程和中间状态,最常用的方式是在程序里有疑问的位置插入一些输出语句,让程序在执行中输出一些变量的值。通过检查某些关键性变量的变化情况,常常可以发现导致程序错误的线索。
Python系统的IDLE与所有功能较强的程序开发环境一样,为动态检查程序提供了很好的支持。我们可以在IDLE里以调试方式执行程序,这时可用的主要功能包括追踪、监视、设置断点、中断执行等。这里先对有关概念做一点简单介绍:
- 追踪:以常规方式执行程序时,程序启动后将一直运行到结束(执行完成而终止,或被强行终止),或者进入死循环。对程序进行追踪,则是以受控方式执行它。例如,可以要求一个个地执行程序里的语句(单步执行),或者要求程序执行中暂停在某个特定位置(中断执行)等。在受控执行中,可以很方便地检查程序执行的中间状态,以及在执行过程中一些变量的变化情况,有助于发现程序里的问题。
- 设置断点:在启动追踪前标出程序里的一些位置,要求程序执行到达这些位置时停下来接受检查。这样可以很方便地检查当时执行现场的各种情况(变量的值)。程序在断点暂停后,可以命令其继续执行或者结束。
- 中断执行:当发现(或认为)程序进入非正常状态,或在程序执行中需要检查中间状态时,可以通过中断命令中断程序执行,让程序停在当时的执行点,但仍处于执行状态,以便检查。使用IDLE时,可以用Ctrl-C中断程序执行。
第4章将介绍IDLE支持程序调试的功能。
IDLE这类支持编程的工具被称为集成开发环境(Integrated Development Environment,简记为IDE),它们集成了辅助编程、运行、测试和调试程序的多方面功能,可以为编程提供很多帮助。在学习编程时,也应该学习和掌握这类工具。不同IDE可能各有特点,但在对程序开发和调试的支持方面差别不大,掌握一个就可以触类旁通。
当然,再好的IDE也只是工具。如果能熟练使用,有利于帮助我们发现程序错误的线索,但确认和改正错误,还必须靠人自己动脑动手。IDE虽然可以使编程工作更方便,但它不会改变编程工作的实质。也应该看到事物的另一面:好的IDE不能造就优秀的程序工作者。现在的IDE越来越强大,但很多人用它们编出的程序质量却很差。所以,编好程序的最基本因素仍然是人,不是更好的工具。
要编好程序,最重要的还是要理解这一工作中的规律性,建立良好的编程习惯,采用正确的工作方法,积累程序设计的经验。这些都是至关重要、不可替代的。程序不是代码的堆积,编程中一个最重要的问题就是程序的设计和组织,程序越大,这方面工作的地位和作用就越明显。良好编写的程序,不仅更容易做正确,发现了错误也更容易定位,容易修改和维护,容易升级改造。本书后面将通过讨论和例子反复强调这一问题。
关于测试,还有一个重要问题。荷兰计算机科学家Dijkstra(图灵奖获得者)有一句名言:测试可以发现一个程序里有错误,但是不能确认其中没有错误。一个程序是否正确,是一个非常深刻的问题,关于这个问题,既有许多理论研究,也有许多实际的方法研究。在进入程序设计这个世界之前,请大家首先记住这一点。
1.3.3 从问题到程序
选择了Python作为程序语言,应该如何着手编写程序呢?程序设计是一种智力劳动,编程序就是以计算的方式解决问题。初学编程时要解决的问题很简单,类似于一道数学或物理应用题,当然要做的是编程,要求完成一个符合题目要求的程序。
一般说,用编程方式解决问题的过程可以分为三步(请重看图1.6):第一步是分析问题,设计一种解决方案;第二步是用程序语言严格描述这个解决方案;第三步是在计算机上试用这个程序,看它是否真的能解决问题。如果发现错误,就需要分析错误原因,弄清问题后回到前面步骤去纠正错误。
工作的第一步与在其他领域里解决问题类似,只是考虑问题的基础不同。做程序设计时需要从计算和程序的观点出发,这里有许多新问题,是本书讨论的一个重点。第二步和第三步是编程的特殊问题。语言中各种结构有明确定义的功能,把头脑中形成的解决方案变为程序,往往也不是直截了当的,需要仔细考虑和规划。进一步说,用符合语言规定的结构和形式写出程序,也有不少工作要做,这个过程中也可能犯错误。前面关于程序中的可能错误与排除的讨论,主要关注第二步和第三步之间的小循环,这方面有许多新东西需要学习。如果测试中发现是问题的解决方案本身有错误,就需要回到第一步了。
在编程领域,在解决小问题与解决大问题之间,为完成课程练习而写程序,与为解决实际问题而写程序之间并没有一条鸿沟。在开发实际程序或软件系统时,前期工作的比重将大大增加:首先需要把问题分析清楚,弄明白到底要做什么。本书中讨论的,并通过实例反复展示的理论、技术和思考问题的方法,同样适用于复杂软件的开发过程。
程序的分解和抽象
还有一个问题值得提出:同样一个程序有可能在不同的层次上描述。这里还用日常生活中的程序性活动作为例子,考虑前面学生早晨的活动过程。例如1.1.2节中的活动描述提到刷牙,那里只用一个词描述这个动作。但如果仔细想想,刷牙也是一个复杂过程,可以进一步将其分解为取杯子、装水、取牙刷、挤牙膏、漱口、刷牙、清洗牙齿等一系列细节动作。如果需要,还可以进一步将上述每个动作分解为一系列的肌肉动作。
最终的程序细节需要分解到哪个层次,依赖于编程语言提供的基本功能。但另一方面,程序的描述方式也要照顾到人的需要。复杂的程序可能包含许多功能,直接在语言的基本层面上描述层次太低,程序的意义很难把握,很难保证实现所预想的功能,也难修改程序去满足新需要,就像看到列出极长的一系列有关肌肉伸缩动作的描述,很难理解这个人做的是刷牙动作一样。因此,在开发复杂程序时,应该采用高层次的描述,把程序功能在各层次上逐步分解。随着程序变得越来越复杂,其组织结构问题也会变得越来越重要。
还是用生活中的例子来说明问题。对于学生早上起床后的活动,我们首先应该在很高的层次上描述,就像前面所给出的:
- 起床
- 刷牙
- 洗脸
- 吃早饭
- 去教室上课
这个描述把一个复杂程序分解为若干相对简单的部分。如果需要进一步细化,那就降到下一层次,把一个高层动作分解为一系列相对低层的基本动作。例如,高层的“吃早饭”动作有可能进一步分解为下面的动作序列:
5.1 拿饭卡
5.2 去食堂
5.3 排队买饭
5.4 吃早饭
5.5 结束和清理
5.6 离开食堂
必要时再做进一步分解。例如将“排队买饭”分解为“排队、选饭、选菜、付款”等。在这种分解过程中,应该保留已有的抽象层描述。这种层次结构有助于理解程序的全局和细节,帮助发现程序错误,使其易于根据需要修改。例如,假设学校食堂改为快餐店,由于整个程序已分解为一些独立的步骤,修改起来也会容易一些。
为计算机编程序也需要这种工作方式,编程者需要从问题的需求出发,从高层开始设计程序,然后逐步分解程序功能。分解到一定细节程度后,就可以用编程语言的已有结构直接描述了。这是分析和构造程序的正确方法。后面将仔细讨论这些问题。
与之对应的,编程语言也应该为程序的分层构造提供支持。参考前面的讨论,作为用于写程序的编程语言,必须包含下面两方面的基本构成要素:
1)需要有一组基本操作,作为复杂计算活动的实现基础。机器和汇编语言里的基本操作就是各种基本指令,高级语言也提供了一组基本操作。
2)需要一套描述计算的流程如何进行的组合机制。机器和汇编语言里的基本机制是顺序执行,以及完成有条件转移或无条件转移的专门指令。高级语言也需要提供一套用于组合简单计算,构造出任意复杂的计算描述的结构。
从理论上说,上面两类要素的组合足以构造出任意复杂的程序。但从实践的角度看,只有它们,实际开发者很难(或说几乎不可能)写出很复杂的程序。为了支持复杂程序的开发,语言中需要第三类机制:抽象机制,其直接作用就是把一些复杂的功能包装成为一个整体,用于支持程序的分层次构造。Python为此提供了计算过程的抽象机制和数据抽象机制,有关细节将在后面章节里仔细讨论。
编程能力
本书及相关课程涉及多方面的能力锻炼,包括知识记忆和灵活运用,解决问题的思维方法,具体处理方法和技巧,实际工作和操作技能。下面列举几个重要方面:
1)分析问题的能力,特别是从计算和程序的角度分析问题的能力。需要学会从问题出发,通过逐步分析和分解,把原问题转化为能用计算机通过程序方式解决的问题,在此过程中设计出解决方案。这方面的深入没有止境。各个领域的问题都需要用计算机解决,参与者既需要熟悉计算机,也需要熟悉专业领域。将来的世界特别需要这种兼容并包的人才。虽然教科书里的问题很简单,但它们也是通向复杂问题的桥梁。
2)掌握所用的程序语言Python。语言是编程的基本工具,要写好程序,必须熟悉所用的语言,熟悉其中的各种结构,它们的形式和意义。应该注意,熟悉语言绝不是背诵定义,这里说的熟悉只有在程序设计的实践中才能完成。就像只是上课和在岸上比划,做的再多也不能学会游泳一样,只是看书、读程序、抄程序不可能真正学会写程序。学习编程,必须反复地亲身实践从问题到程序的整个过程,动脑筋想办法,处理遇到的各种情况。如前所述,目前人们常用于软件开发的编程语言不止Python一种。但各种语言有很多共性,学习了一种之后可以作为参照。另一方面,在用一种语言学习编程的过程中积累的一般性知识和经验,在用任何语言开发程序时都可以参考。
3)学会写程序。虽然写过程序的人很多,但会写程序、能写出好程序的人就少得多了。经过多年程序实践,人们对“好程序”有了许多共识。例如,解决同样问题的程序越简单越好。这里可能有计算方法的选择问题,有语言的使用问题。除了程序正确外,人们也特别关注程序是否结构良好,是否清晰,易阅读和理解,条件或要求改变时是否容易修改去满足新需要等。后面将反复提到这些问题。
4)检查程序错误的能力。初步写出的程序经常包含一些错误。虽然解释器能帮助查出其中一些,并通告发现错误的位置,但确认实际错误和实际位置,弄清应该如何改正,永远是编程序的人自己的事情。对系统报告的运行错误,死循环或逻辑错误等的认定,更要依靠人的能力。这种能力也需要在学习中培养和锻炼。
5)熟悉所用工具和环境。程序设计要用一些编程工具,要在具体的计算机环境中进行,熟悉工具和环境也是这个学习中很重要的一部分。本书建议用IDLE做程序实习,熟悉这个环境的使用也很重要,可能大大提高工作效率。不同的编程工具之间也有很多共性,学习了一种工具,对理解掌握其他工具也将很有帮助。
后面各章将逐步展开有关计算和程序设计的讨论:从最简单的计算问题、最简单的数据描述和简单表达式开始,讨论在Python中写简单程序的情况。而后讨论程序的基本流程结构,以及如何用这些结构解决更复杂一些的计算问题。然后介绍程序的组织和抽象,以及Python语言为计算抽象提供的基本机制——函数,还要介绍Python程序的基本结构和一些内在的道理。随后的讨论将转向数据的组织,介绍Python的各种数据组织功能。在复杂的计算中,需要处理的数据也更加复杂多样,需要采用适当的方式将它们组织起来。Python语言为数据组织提供了一套标准功能,如果需要,我们还可以自己定义数据的组织方式。在讨论面向对象的第7章里,将仔细讨论这方面的重要思想和应用技术。本书最后部分还将讨论一些更具体的而且也很重要的编程领域和问题。