少儿 Python 编程 _ 第二十讲:编程技巧

在学习编程的过程中,起初新手开发者对程序没有什么概念,先讲解习惯和注意事项,不但没什么效果,注意事项太多,反而提高了编程的难度;往往在自己遇到问题后,经过思考,印象更加深刻。之前在每一讲课后练习中,也加入了一些技巧说明,但比较分散。本讲将总结编写程序过程中遇到的各种问题和编程习惯。

20.1 编程习惯

写程序最重要的是实现功能,在实现功能的基础上,好的编程习惯,让代码更清晰,更容易理解,无论是过一段时间自己再看,还是给别人使用都能节约大量时间;同时,好的编程习惯让代码在不同运行环境和操作系统中也能稳定地运行。

20.1.1 缩进

缩进指代码与边界之间的距离,Python 使用缩进组织代码块,一般用冒号和缩进区分代码之间的层次。代码块缩进常出现在:函数体、循环体、以及判断语句之后。

对 Python 编程来说,缩进是必不可少的;其它编程语言,也大都包括缩进,但有的不是必须缩进,比如 C 语言用大括号括住循环体内容,但一般程序员也会使用空格缩进,这样更容易看到代码的层次:直观地看到循环从哪里开始,到哪里结束,缩进也是一种良好的编程习惯。

1.缩进相关错误

1
<pre style="tab-stops:21.75pt 91.6pt 137.4pt 183.2pt 229.0pt 274.8pt 320.6pt 366.4pt 412.2pt 458.0pt 503.8pt 549.6pt 595.4pt 641.2pt 687.0pt 732.8pt">  
缩进的英文是 Indentation,在报错信息中看到相关的单词,如“unexpected indent”,就需要注意,可能是缩进错误。
1
<pre style="tab-stops:21.75pt 91.6pt 137.4pt 183.2pt 229.0pt 274.8pt 320.6pt 366.4pt 412.2pt 458.0pt 503.8pt 549.6pt 595.4pt 641.2pt 687.0pt 732.8pt">  
有一种常见的错误是:缩进中间断开,如下例所示:
1
2
3
4
01 if x>0:
02     print("x>0")
03 print(x)
04     print("ok")

编写这段语句的本意是:需要在 x 大于 0 时打印 02 和 04 句,无论什么情况都打印第 03 句。但使用以上写法,第三句没有缩进,表示 if 条件对应的代码块在 02 句之后已经结束,第 04 句的缩进出错。

冒号表示一个新代码块的开始,行尾的冒号和新增的缩进一一对应,只有冒号没有新的缩进,或者没有冒号但有新的缩进都是错误的,代码块中间不能断开。

正确的方法是调换第 03 句和 04 句的位置。

2.缩进的空格数

缩进使用的空格数是可变的,一般情况下,使用四格缩进,但也有人习惯两空格缩进或八空格缩进。在同一程序中必须使用一致的缩进规则,建议使用默认的四格缩进。

3.Tab 键生成缩进和空格生成缩进

Tab 键输入制表符,用于文本对齐,制表符在不同软件中被显示成四个或者八个空格。在 Jupyter Notebook 或者 Spyder 等编程工具中,使用 Tab 键生成的制表符都被转换成了对应的空格,因此可以使用 Tab 键生成缩进。但使用 Windows 记事本这类纯文本编辑器时,不自动转换成空格,如果混用 Tab 键输入的制表符和空格,程序运行时就会报错,由于制表符和空格看起来差不多,这种错误很难通过观察发现。

20.1.2 命名

无论是编写程序还是使用文件都会遇到命名问题,比如函数名、类名、变量名、文件名、路径名、网址、数据库的字段名等等。计算机上的命名规则也大同小异。一般都支持英文字母、数字、下划线,有的命名也支持中文、特殊符号、空格等等,虽然有些命名规则支持上百个字符的命名长度,但习惯上一般都在 20 字符以内,不应太长。

1.有意义的命名

命名最重要的是有意义,让自己和他人通过名字可以了解变量或者文件的作用。尽量不要使用 a、b 这样的命名,很难查找,时间长了自己也会忘记用途。

2.中文和特殊符号

Python 3 之后的版本,支持用中文命名变量,Windows 也支持用中文命名文件和目录。由于在计算机中支持多种中文编码,使用不同软件或者系统打开文件时,默认的编码方式如果不同,则可能出现乱码;另外,有些系统区分大小写字母,有的则视大小写为同一字母。

在编写程序时,尽量使用小写英文字母数字和下划线命名,最好使用有意义的英文单词,其次是使用拼音,尽量少使用中文命名变量、函数和类,除了下划线“_”,命名时也尽量少使用其它的特殊符号和空格,如果想用一个以上的词命名变量,建议使用下划线分隔词,例如“get_width”相比“getwidth”更加直观。

另外,还需要注意区分下划线“_”和减号“-”;中英文标点符号不同;一些字符看起来比较像,比如大写的 i(I)和小写的 L(l),英文字母 O 和数字 0 比较容易混淆等等。

3.命名习惯

注意根据用途命名,例如:主要给老师、同学、家长(中国人)看的文件名、目录名可以用中文命名;给程序员看的代码文件名、代码中的内容、数据库字段使用英文字母命名;提供给别人调用的文件或者函数命名要有意义,只供自己使用的内部变量可相对随意一些。

另外,还有一些约定俗成的用法,比如:一般使用字母 i,j 表示循环中的记数变量。对于一些不变的值,通常使用大写字母命名,比如窗口的长宽常使用 WIDTH 和 HEIGHT 命名。

20.1.3 注释

1.用途

注释是对代码的解释和说明,它的目的是让人能够更加轻松地理解代码。虽然使用有意义的命名可以描述程序文件、函数、变量的功能,但命名不能太长,一般通过注释进行更加详细的说明。注释有以下几种使用场景:

  • 在程序文件的开头介绍该程序的功能和用法。
  • 在函数之前介绍函数的功能和用法。
  • 在难以理解的语句前后,说明语句的功能。
  • 将暂时不用的代码注释掉。

注释要尽量写得简洁而清楚,如果不熟悉英文,就用中文写,内容比形式更重要。不光自己看得懂,也要让别人能看懂。

2.用法

Python 使用井号“#”实现单行注释,当前行中井号之后的内容都视为注释。使用三引号实现多行注释,形如:

1
2
3
4
5
01 print('aaa') # 打印信息
02 """
03 注释第一行
04 注释第二行
05 """

三引号用于定义包含回车换行的字符串,可以是三个单引号,也可以是三个双引号。在程序中的字符串,不操作、不赋值,也不影响程序的运行,因此,常作为包含多行的注释使用。

20.2 调试程序

学习自然语言时,除了记住词义以外,还要能组合成句子文章,才能正常使用。编程语言也是一种语言,学习时不仅要记住所学知识点,还要学习程序整体的构造和调试。在每一讲后面的习题可供读者练习编写各种程序。尤其是从第十五章之后,不再对课程中实例的简单修改,而是用完全不同的代码构造新的功能。

如果读者按要求完成了练习,在练习过程中一定遇到了很多的程序错误,也从中学习了解决问题的方法。

本小节将总结常见的问题和解法,并介绍简单有效的程序调试方法。

20.2.1 常见问题

无论是使用命令行还是 Python 集成开发环境,程序运行出错时都会显示错误提示。提示信息中最重要的是行号信息,开发者通过行号可以确定错误的大概位置。

1.语法错误

图20.1 语法错误示例

图 20.1 是一种常见的错误,“invalid syntax”意思是语法错误,提示错误出现在第二行(line 2),实际上,错误的原因是第一行少了右括号,程序一直在等待右括号,而在第 02 行却出现了 print 语句,所以提示为第 02 行错误。由此可见,也有少量错误行提示,可能是由之前行的错误引起的。但至少可以通过行号确认大概位置。

2.缩进错误

图 20.2 是缩进错误,“unexpected indent”意思是意外的缩进,它由第 2 行多了一个空格引起,也是常见的输入引起的错误。

图20.2 缩进错误

3.名字未定义错误

图 20.3 示例了未定义错误,程序在使用 y 之前,没有定义 y,因此提示“name ‘y’ is not defined”(名字’y’没有被定义)。有时候,并不是使用变量或者函数前没有定义,也可能是没有引入函数所在的三方库、或者是输入时拼写错误引起的,比如:混淆字母 O 和数字 0,逗号看成句号,冒号和分号写错等等。像大写 I(i) 和小写 l(L) 混淆的错误很难被观察到,这时,可以使用查找功能,比如在 Jupyter Notebook 中用 Ctrl+f 查找报错中提示的名字。

图20.3未定义错误

4.下标越界错误

图 20.4 示例了下标越界错误,报错为“list index out of range”(列表索引号超出范围),程序在第 1 行定义了含有三个元素的列表 arr,在第 2 行试图访问数组的第 4 个元素(索引号为 3),数组元素索引号从 0 开始,范围是 0,1,2,本例中访问的索引号超过了列表的最大索引号。

图20.4 下标越界错误

5.其它问题

程序可能出现的错误非常多,无法一一列举,而报错信息一般都是英文的,建议使用以下步骤查找错误原因:

首先,查看错误提示中的行号,以及行号之前的程序,看是否能发现代码错误。

如果未找到原因,查看是否为以上几种错误类型。

如果不是以上问题,用翻译工具翻译错误提示,以定位问题。

如果仍不能找到问题,在搜索引擎(如百度)中搜索错误提示,查看他人解决此类问题的方法。

20.2.2 调试方法

在程序中间加入断点和打印信息都是标准的程序调试方法,特别是在运行他人编写的代码时,用这种方法可以了解每个阶段做了什么。

我们写的程序都相对简单,类和函数并不多,当运行别人代码时,梳理互相调用关系很重要,最好能画出简单的调用关系图。

1.print 函数

Python 中的 print 函数用于打印输出,它虽然不是专用的程序调试工具,但是如果养成使用 print 跟踪程序运行的习惯,至少一半的问题都可以解决。

学习使用 print 调试程序不是学习函数的使用方法,而是在编写程序的过程中,要理解程序每一行做了什么工作,每一行程序执行之后当前环境中各个变量的状态是什么,尤其是在循环这样较为复杂的结构中,需要了解每一次循环中数据的变化。而使用 print 语句则是在开发者不太清楚当前状态的情况下,用程序输出所关注的变量或者数据。

比如加载数据表文件之后,可以用 print 语句显示加载后的内容以及数据格式,又如跟踪循环中数据的变化情况。如下例所示。

1
2
3
4
01 s = 0
02 for i in [1,2,3]:
03     s = s + i * 7
04     print(s, i)

其中第四行用于显示每一次循环中的 s 值和 i 值。当程序并未报错,但输出结果与想象中不同时,可以使用 print 方法跟踪程序中数据的变化,以定位出错的位置。

还有一种常用的调试方法是注释掉可能错误的程序段,然后运行程序,如果运行仍不正常,说明不是被注释掉语句的问题,继续注释更多的语句;如果注释后运行正常,则说明是该段问题,然后尝试注释掉较少的语句,直到定位到具体出错的行。

2. 调试工具 pdb

pdb 是 Python 的调试工具,由于 pdb 使用方法比较复杂,此处讲解 pdb 只作为知识扩展,不要求读者掌握。

在 Jupyter 中加入魔法命令%pdb,即可在程序出错时调用 pdb,以便调试出错时的具体代码。例如,运行以下程序:

1
2
3
01 %pdb
02 arr = ['a','b','c']
03 print(arr[3])

程序运行到第三行时,会报错,但并未退出,界面上将出现可交互的输入框,开发者可以在输入框中输入程序如:print(arr),来进一步运行程序。请注意:调试完成之后需要在输入框中输入 exit 退出调试模式。

使用 pdb 的另一种方法是在程序中加入断点,当程序运行到该行,会跳出可交互的输入框,开发者可以通过 pdb 命令查看当前状态,或者逐步执行断点之后的程序。

设置断点的方法是,在程序中加入 pdb.set_trace(),如以下程序所示:

1
2
3
4
01 import pdb
02 arr = ['a','b','c']
03 pdb.set_trace()
04 print(arr[3])

pdb 常用的调式命令如下:

  • 单步调试(进入函数):s(tep)。
  • 单步调试(不进入函数):n(ext)。
  • 继续往后执行,直到下个断点:c(ont(inue))。
  • 运行到函数结束:r(eturn)。
  • 运行到当前循环结束:unt(il)。
  • 设置断点:b(reak) 文件名: 行号(或行号,或函数名)。
  • 显示当前调用关系:w(here)。
  • 显示当前代码段:l(ist)。
  • 显示变量:p(rint) 变量名。
  • 显示当前函数的参数:a(rgs)。
  • 显示帮助信息:h(elp)。
  • 退出:q(uit)。

20.2.3 错误处理

程序有时可能出现一些难以预料的错误,比如写客户端程序与服务器端交互,就可能遇到很多问题,如网络未连接,对三方库调用方法不对,与服务器数据交互格式不对,或者以前能正常运行,后来服务端修改了端口,找不到对应功能等等。

这种情况下,程序员,尤其是写程序供他人调用的程序员,需要识别出程序中可能出现的错误,进行错误处理后,保证程序正常运行,而不会意外退出。

当程序运行出错时会抛出异常,如果不做处理,则程序会异常退出,Python 用 try/except 方式捕获异常,其语法规则如下:

1
2
3
4
5
6
7
8
01 try:
02     程序代码
03 except <异常类> as <变量>:
04 异常处理代码
05 else:
06     异常以外其它情况处理
07 finally:
08     无论是否异常,最终都要执行的代码

简单实例如下:以读方式打开文件 test.txt,该文件不存在时将抛出异常,例程中捕获异常,并显示出具体的异常信息。

1
2
3
4
5
6
7
8
01 try:
02     f = open('test.txt', 'r')
03 except Exception as e:
04     print('error', e)
05 print('aaaa')
06 # 返回结果:
07 # error [Errno 2] No such file or directory: 'test.txt'
08 # aaaa

从返回结果可以看到,捕捉到异常信息后,程序正常执行了之后的打印信息操作,而并未因为异常而崩溃。

课后练习:(练习答案见本讲最后的小结部分)

练习一:尝试捕获越界的错误。

20.3 思维训练

20.3.1 学习编程的好处

如果读者从头到尾学习了所有例程,并做完了所有习题,可能发现以下变化:

  • 熟悉了程序、界面、网络、数据库、数据分析等概念。
  • 熟练使用 Python 编程环境。
  • 对大多数问题都能定位到关键点。
  • 写程序从修改变成了构建。
  • 不再抵触较长的程序段。
  • 变量命名更有章法。
  • 更喜欢使用工具和快捷键。
  • 能解决大多数语法方面的问题,至少有了解决思路。

……

12 岁以下的学生,对于文中绝大多数的概念都是第一次接触,即使当时学会了,过一段时间也会淡忘,因此推荐整体看三遍以上,最终目标是能自如地编写练习中的每一个程序,并且能够将程序用于日常的数据处理。

20.3.2 防患于未然

之前提到学习常常分为两部分:一部分从经验中学习,即构建框架;另一部分从教训中学习。经验可以通过实践或者学习规则构造,而教训更多往往是自己犯了错误印象才深。

教训又可再细分成两种情况:改正错误和防患于未然。发现问题及时改正可能导致处理过程的暂停和回退;而防患于未然则更多地源于之前的积累,它们在错误的行为之前就进行了阻止,而从表面看来,似乎非常平稳,未经波折。实际上是经验在毫无意识的情况下就发生了作用。比如我们不会在公共场景提到禁忌话题。

因此,很多时候看起来运气比较好或者天生的灵感,实际上下意识的行为是源于之前的积累。对于一个严谨的人,很多思路在没有外显时可能就已经被过滤掉了,往往也显得没有幽默感,缺少想象力。严谨或者随意可能取决于个人经历和环境,也可能是遗传或者性格所致。

人们不能要求小孩子对陌生人又友好,又有戒心,但是有些成年人却可以做到。我们在成长过程中不断打磨,逐渐学会了在两极之间取折中的方案,在不同的情况下使用不同的框架。

20.3.3 注意事项

最后,整理了一些学习编程中的注意事项,与大家共同学习:

  • 任何老师都不可能列出所有可能出现的问题,要学会在试错中进步。
  • 基础知识非常重要,无论文科理科记和背都是必须的,对常用知识构建条件反射,才能有更多的脑力用于后续的学习和思考。
  • 完成功能后整理代码:从变量命名到代码顺序,都请严格要求自己。
  • 量的问题积累太多,就变成了质的问题:一个小问题,努力一下也许能解决,但是十个小问题堆叠起来,脑子就直接转不动了。
  • 代码重构比代码填空难度大得多,尽量多练习构建整体代码逻辑。
  • 分类和对比非常重要,如果不能直接找到答案,可以寻找类似的情况及处理方法。
  • 代码需要逐行读懂,长代码,很可能读到后面忘了前面,建议边读边记,最好能绘制流程图,有时候也需要反复阅读。
  • 初学者对例程往往知其然,不知其所以然,在看他人代码时尽量动手跟踪调试程序。
  • 写程序像弹琴一样需要不断练习,即使学一遍就会,后边不用也会很快忘记。

20.4 小结

20.4.1 单词

本讲需要掌握的英文单词如表 20.1 所示。

表20.1本讲需要掌握的英文单词

20.4.2 习题答案

  1. 练习一:尝试捕获越界的错误。
1
2
3
4
5
6
01 try:
02     arr=['a','b','c']
03     print(arr[3])
04 except Exception as e:
05     print('error',e)
06 print('aaaa')