AD

Mark Lutz:Python程序员的常见错误

在这篇文章中,我将总结新老Python程序员常犯的一些错误,以帮助你们在自己的工作避免犯同样或类似错误。

首先我要说明一下的是,这些都是来源于第一手的经验。我以讲授Python的知识为生。在过去的7年里,我已经给上千名学生讲授上百堂Python的课程,同时看着这些学生们犯同样的错。也就是说,这些是我看着Python初学者活生生犯的错,千百次的错。事实上,这些错误实在是太普遍了以至于我敢保证你刚开始学的时候是一定会犯的。

“那么是什么呢?”你会问,“你也会在Python里犯那么多错么?”是的。Python可能是最简单、最灵活的语言之一,但它终究还是一门编程语言。它仍然有语法,数据类型,以及巫师蒂姆居住的黑暗角落。

(典故出自《蒙蒂派森与圣杯》中的魔法师蒂姆,他主角们指点在洞穴的墙壁上记录的圣杯位置,作者在此处的意思是Python语言里容易犯错的地方。另,Python语言得名于作者Guido van Rossum特别喜欢的《蒙蒂派森飞行马戏团(Monty Python’s Flying Circus)》——译者注)

好事情是多亏了Python那干净的设计,一旦你学会了Python,你就能自动的避开很多陷阱。Python在其各组件之间有着最小的互动,这能有效的减少bug。它也拥有十分简单的语法,这意味着在一开始你就有更小的概率犯错。当你实在是犯了错的时候,Python的即时错误检测和报告能帮你迅速的恢复。

但用Python编程也不是个自动完成的活儿,很多事还是要早做准备。那么废话不多说了,让我们直切正题。在接下来的三节里我们将这些错误分为语用、代码,以及编程三个大类。如果你想读到更多的Python的常见错误以及如何避免它们,那么在O’Reilly系列丛书的《Learning Python》里有详细的解读。(译注:Learning Python 已经是第五版了)

语用错误

让我们从基础开始,从那些刚学习编程的人钻研语法之前碰到的事情开始。如果你已经编过一些程了,那么以下这些可能看起来十分的简单;如果你曾经尝试过教新手们怎么编程,它们可能就不这么简单了。

在交互提示符中输入Python代码

在>>>交互提示符中你只能输入Python代码,而不是系统命令。时常有人在这个提示符下输入emacs,ls,或者edit之类的命令,这些可不是Python代码。在Python代码中确实有办法来调用系统命令(例如os.system和os.popen),但可不是像直接输入命令这么直接。如果你想要在交互提示符中启动一个Python文件,请用import file,而不是系统命令python file.py。

Print语句(仅仅)是在文件中需要

因为交互解释器会自动的讲表达式的结果输出,所以你不需要交互的键入完整的print语句。这是个很棒的功能,但是记住在代码文件里,通常你只有用print语句才能看得到输出。

小心Windows里的自动扩展名

如果你在Windows里使用记事本来编辑代码文件的话,当你保持的时候小心选择“所有文件”(All Files)这个类型,并且明确的给你的文件加一个.py的后缀。不然的话记事本会给你的文件加一个.txt的扩展名,使得在某些启动方法中没法跑这个程序。更糟糕的是,像Word或者是写字板一类的文字处理软件还会默认的加上一些格式字符,而这些字符Python语法是不认的。所以记得,在Windows下总是选“所有文件”(All Files),并保存为纯文本,或者使用更加“编程友好”的文本编辑工具,比如IDLE。在IDLE中,记得在保存时手动加上.py的扩展名。

在Windows下点击图标的问题

在Windows下,你能靠点击Python文件来启动一个Python程序,但这有时会有问题。首先,程序的输出窗口在程序结束的瞬间也就消失了,要让它不消失,你可以在文件最后加一条raw_input()的调用。另外,记住如果有错的话,输出窗口也就立即消失了。要看到你的错误信息的话,用别的方法来调用你的程序:比如从系统命令行启动,通过提示符下用import语句,或者IDLE菜单里的选项,等等。

Import只在第一次有效

你可以在交互提示符中通过import一个文件来运行它,但是这只会在一个会话中起一次作用;接下来的import仅仅是返回这个已经加载的模块。要想强制Python重新加载一个文件的代码,请调用函数reload(module)来达到这个目的。注意对reload请使用括号,而import不要使用括号。

空白行(仅仅)在交互提示符中有作用

在模块文件中空白行和注释统统会被忽略掉,但是在交互提示符中键入代码时,空白行表示一个复合语句的结束。换句话说,空白行告诉交互提示符你完成了一个复合语句;在你真正完成之前不要键入回车。事实上当你要开始一个新的语句时,你需要键入一个空行来结束当前的语句——交互提示符一次只运行一条语句。

代码错误

一旦你开始认真写Python代码了,接下来了一堆陷阱就更加危险了——这些都是一些跨语言特性的基本代码错误,并常常困扰不细心的程序员。

别忘了冒号

这是新手程序员最容易犯的一个错误:别忘了在复合语句的起始语句(if,while, for等语句的第一行)结束的地方加上一个冒号“:”。也许你刚开始会忘掉这个,但是到了很快这就会成为一个下意识的习惯。课堂里75%的学生当天就可以记住这个。

初始化变量

在Python里,一个表达式中的名字在它被赋值之前是没法使用的。这是有意而为的:这样能避免一些输入失误,同时也能避免默认究竟应该是什么类型的问题(0,None,””,[],?)。记住把计数器初始化为0,列表初始化为[],以此类推。

从第一列开始

确保把顶层的,未嵌套的代码放在最左边第一列开始。这包括在模块文件中未嵌套的代码,以及在交互提示符中未嵌套的代码。Python使用缩进的办法来区分嵌套的代码段,因此在你代码左边的空格意味着嵌套的代码块。除了缩进以外,空格通常是被忽略掉的。

缩进一致

在同一个代码块中避免讲tab和空格混用来缩进,除非你知道运行你的代码的系统是怎么处理tab的。否则的话,在你的编辑器里看起来是tab的缩进也许Python看起来就会被视作是一些空格。保险起见,在每个代码块中全都是用tab或者全都是用空格来缩进;用多少由你决定。

在函数调用时使用括号

无论一个函数是否需要参数,你必须要加一对括号来调用它。即,使用function(),而不是function。Python的函数简单来说是具有特殊功能(调用)的对象,而调用是用括号来触发的。像所有的对象一样,他们也可以被赋值给变量,并且间接的使用他们:x=function:x()。
在Python的培训中,这样的错误常常在文件的操作中出现。通常会看到新手用file.close来关闭一个问题,而不是用file.close()。因为在Python中引用一个函数而不调用它是合法的,因此不使用括号的操作(file.close)无声的成功了,但是并没有关闭这个文件!

在Import时不要使用表达式或者路径

在系统的命令行里使用文件夹路径或者文件的扩展名,但不要在import语句中使用。即,使用import mod,而不是import mod.py,或者import dir/mod.py。在实际情况中,这大概是初学者常犯的第二大错误了。因为模块会有除了.py以为的其他的后缀(例如,.pyc),强制写上某个后缀不仅是不合语法的,也没有什么意义。
和系统有关的目录路径的格式是从你的模块搜索路径的设置里来的,而不是import语句。你可以在文件名里使用点来指向包的子目录(例如,import dir1.dir2.mod),但是最左边的目录必须得通过模块搜索路径能够找到,并且没有在import中没有其他路径格式。不正确的语句import mod.py被Python认为是要记在一个包,它先加载一个模块mod,然后试图通过在一个叫做mod的目录里去找到叫做py的模块,最后可能什么也找不到而报出一系列费解的错误信息。

不要在Python中写C代码

以下是给不熟悉Python的C程序员的一些备忘贴士:

  • 在if和while中条件测试时,不用输入括号(例如,if (X==1):)。如果你喜欢的话,加上括号也无妨,只是在这里是完全多余的。
  • 不要用分号来结束你的语句。从技术上讲这在Python里是合法的,但是这毫无用处,除非你要把很多语句放在同一行里(例如,x=1; y=2; z=3)。
  • 不要在while循环的条件测试中嵌入赋值语句(例如,while ((x=next() != NULL))。在Python中,需要表达式的地方不能出现语句,并且赋值语句不是一个表达式。

编程错误

下面终于要讲到当你用到更多的Python的功能(数据类型,函数,模块,类等等)时可能碰到的问题了。由于篇幅有限,这里尽量精简,尤其是对一些高级的概念。要想了解更多的细节,敬请阅读Learning Python, 2ndEdition的“小贴士”以及“Gotchas”章节。

打开文件的调用不使用模块搜索路径

当你在Python中调用open()来访问一个外部的文件时,Python不会使用模块搜索路径来定位这个目标文件。它会使用你提供的绝对路径,或者假定这个文件是在当前工作目录中。模块搜索路径仅仅为模块加载服务的。

不同的类型对应的方法也不同

列表的方法是不能用在字符串上的,反之亦然。通常情况下,方法的调用是和数据类型有关的,但是内部函数通常在很多类型上都可以使用。举个例子来说,列表的reverse方法仅仅对列表有用,但是len函数对任何具有长度的对象都适用

不能直接改变不可变数据类型

记住你没法直接的改变一个不可变的对象(例如,元组,字符串):


1

2


T = (1, 2, 3)

T[2] = 4 # 错误

用切片,联接等构建一个新的对象,并根据需求将原来变量的值赋给它。因为Python会自动回收没有用的内存,因此这没有看起来那么浪费:


1


T = T[:2] + (4,) # 没问题了: T 变成了 (1, 2, 4)

使用简单的for循环而不是while或者range

当你要从左到右遍历一个有序的对象的所有元素时,用简单的for循环(例如,for x in seq:)相比于基于while-或者range-的计数循环而言会更容易写,通常运行起来也更快。除非你一定需要,尽量避免在一个for循环里使用range:让Python来替你解决标号的问题。在下面的例子中三个循环结构都没有问题,但是第一个通常来说更好;在Python里,简单至上。


1

2

3

4

5

6

7

8


S = "lumberjack"

for c in S: print c # 最简单

for i in range(len(S)): print S[i] # 太多了

i = 0 # 太多了

while i < len(S): print S[i]; i += 1

不要试图从那些会改变对象的函数得到结果

诸如像方法list.append()和list.sort()一类的直接改变操作会改变一个对象,但不会将它们改变的对象返回出来(它们会返回None);正确的做法是直接调用它们而不要将结果赋值。经常会看见初学者会写诸如此类的代码:


1


mylist = mylist.append(X)

目的是要得到append的结果,但是事实上这样做会将None赋值给mylist,而不是改变后的列表。更加特别的一个例子是想通过用排序后的键值来遍历一个字典里的各个元素,请看下面的例子:


1

2


D = {...}

for k in D.keys().sort(): print D[k]

差一点儿就成功了——keys方法会创建一个keys的列表,然后用sort方法来将这个列表排序——但是因为sort方法会返回None,这个循环会失败,因为它实际上是要遍历None(这可不是一个序列)。要改正这段代码,将方法的调用分离出来,放在不同的语句中,如下:


1

2

3


Ks = D.keys()

Ks.sort()

for k in Ks: print D[k]

只有在数字类型中才存在类型转换

在Python中,一个诸如123+3.145的表达式是可以工作的——它会自动将整数型转换为浮点型,然后用浮点运算。但是下面的代码就会出错了:


1

2

3


S = "42"

I = 1

X = S + I # 类型错误

这同样也是有意而为的,因为这是不明确的:究竟是将字符串转换为数字(进行相加)呢,还是将数字转换为字符串(进行联接)呢?在Python中,我们认为“明确比含糊好”(即,EIBTI(Explicit is better than implicit)),因此你得手动转换类型:


1

2


X = int(S) + I # 做加法: 43

X = S + str(I) # 字符串联接: "421"

循环的数据结构会导致循环

尽管这在实际情况中很少见,但是如果一个对象的集合包含了到它自己的引用,这被称为循环对象(cyclic object)。如果在一个对象中发现一个循环,Python会输出一个[…],以避免在无限循环中卡住:


1

2

3

4


>>> L = ['grail'] # 在 L中又引用L自身会

>>> L.append(L) # 在对象中创造一个循环

>>> L

['grail', [...]]

除了知道这三个点在对象中表示循环以外,这个例子也是很值得借鉴的。因为你可能无意间在你的代码中出现这样的循环的结构而导致你的代码出错。如果有必要的话,维护一个列表或者字典来表示已经访问过的对象,然后通过检查它来确认你是否碰到了循环。

赋值语句不会创建对象的副本,仅仅创建引用

这是Python的一个核心理念,有时候当行为不对时会带来错误。在下面的例子中,一个列表对象被赋给了名为L的变量,然后L又在列表M中被引用。内部改变L的话,同时也会改变M所引用的对象,因为它们俩都指向同一个对象。


1

2

3

4

5

6

7

8


>>> L = [1, 2, 3] # 共用的列表对象

>>> M = ['X', L, 'Y'] # 嵌入一个到L的引用

>>> M

['X', [1, 2, 3], 'Y']

>>> L[1] = 0 # 也改变了M

>>> M

['X', [1, 0, 3], 'Y']

通常情况下只有在稍大一点的程序里这就显得很重要了,而且这些共用的引用通常确实是你需要的。如果不是的话,你可以明确的给他们创建一个副本来避免共用的引用;对于列表来说,你可以通过使用一个空列表的切片来创建一个顶层的副本:


1

2

3

4

5

6

7

8


>>> L = [1, 2, 3]

>>> M = ['X', L[:], 'Y'] # 嵌入一个L的副本

>>> L[1] = 0 # 仅仅改变了L,但是不影响M

>>> L

[1, 0, 3]

>>> M

['X', [1, 2, 3], 'Y']

切片的范围起始从默认的0到被切片的序列的最大长度。如果两者都省略掉了,那么切片会抽取该序列中的所有元素,并创造一个顶层的副本(一个新的,不被公用的对象)。对于字典来说,使用字典的dict.copy()方法。

静态识别本地域的变量名

Python默认将一个函数中赋值的变量名视作是本地域的,它们存在于该函数的作用域中并且仅仅在函数运行的时候才存在。从技术上讲,Python是在编译def代码时,去静态的识别本地变量,而不是在运行时碰到赋值的时候才识别到的。如果不理解这点的话,会引起人们的误解。比如,看看下面的例子,当你在一个引用之后给一个变量赋值会怎么样:


1

2

3

4

5

6


>>> X = 99

>>> def func():

... print X # 这个时候还不存在

... X = 88 # 在整个def中将X视作本地变量

...

>>> func( ) # 出错了!

你会得到一个“未定义变量名”的错误,但是其原因是很微妙的。当编译这则代码时,Python碰到给X赋值的语句时认为在这个函数中的任何地方X会被视作一个本地变量名。但是之后当真正运行这个函数时,执行print语句的时候,赋值语句还没有发生,这样Python便会报告一个“未定义变量名”的错误。

事实上,之前的这个例子想要做的事情是很模糊的:你是想要先输出那个全局的X,然后创建一个本地的X呢,还是说这是个程序的错误?如果你真的是想要输出这个全局的X,你需要将它在一个全局语句中声明它,或者通过包络模块的名字来引用它。

默认参数和可变对象

在执行def语句时,默认参数的值只被解析并保存一次,而不是每次在调用函数的时候。这通常是你想要的那样,但是因为默认值需要在每次调用时都保持同样对象,你在试图改变可变的默认值(mutable defaults)的时候可要小心了。例如,下面的函数中使用一个空的列表作为默认值,然后在之后每一次函数调用的时候改变它的值:


1

2

3

4

5

6

7

8

9

10

11

12


>>> def saver(x=[]): # 保存一个列表对象

... x.append(1) # 并每次调用的时候

... print x # 改变它的值

...

>>> saver([2]) # 未使用默认值

[2, 1]

>>> saver() # 使用默认值

[1]

>>> saver() # 每次调用都会增加!

[1, 1]

>>> saver()

[1, 1, 1]

有的人将这个视作Python的一个特点——因为可变的默认参数在每次函数调用时保持了它们的状态,它们能提供像C语言中静态本地函数变量的类似的一些功能。但是,当你第一次碰到它时会觉得这很奇怪,并且在Python中有更加简单的办法来在不同的调用之间保存状态(比如说类)。

要摆脱这样的行为,在函数开始的地方用切片或者方法来创建默认参数的副本,或者将默认值的表达式移到函数里面;只要每次函数调用时这些值在函数里,就会每次都得到一个新的对象:


1

2

3

4

5

6

7

8

9

10

11


>>> def saver(x=None):

... if x is None: x = [] # 没有传入参数?

... x.append(1) # 改变新的列表

... print x

...

>>> saver([2]) # 没有使用默认值

[2, 1]

>>> saver() # 这次不会变了

[1]

>>> saver()

[1]

其他常见的编程陷阱

下面列举了其他的一些在这里没法详述的陷阱:

  • 在顶层文件中语句的顺序是有讲究的:因为运行或者加载一个文件会从上到下运行它的语句,所以请确保将你未嵌套的函数调用或者类的调用放在函数或者类的定义之后。
  • reload不影响用from加载的名字:reload最好和import语句一起使用。如果你使用from语句,记得在reload之后重新运行一遍from,否则你仍然使用之前老的名字。
  • 在多重继承中混合的顺序是有讲究的:这是因为对superclass的搜索是从左到右的,在类定义的头部,在多重superclass中如果出现重复的名字,则以最左边的类名为准。
  • 在try语句中空的except子句可能会比你预想的捕捉到更多的错误。在try语句中空的except子句表示捕捉所有的错误,即便是真正的程序错误,和sys.exit()调用,也会被捕捉到。
  • 兔子可能会比他们看起来更加危险。(原句Bunnies can be more dangerous than they seem. 意思是一些看起来比较细微的问题实际上可能更危险。——译者注)

作者Mark Lutz系世界领先的Python教育者,Python最早的畅销教材的作者,并且从1992年开始便长期贡献于Python社区。

标签: Linux社区
分类: 编程
时间: 2013-12-24

相关文章

  1. Python程序员的10个常见错误

    关于Python Python是一门解释性的,面向对象的,并具有动态语义的高级编程语言.它高级的内置数据结构,结合其动态类型和动态绑定的特性,使得它在快速应用程序开发(Rapid Application Developm ...
  2. Python程序员开发中常犯的10个错误

    这篇文章主要介绍了Python程序员开发中常犯的10个错误,不知道你有没有中枪呢,需要的朋友可以参考下 Python是一门简单易学的编程语言,语法简洁而清晰,并且拥有丰富和强大的类库.与其它大多数程序设计语言使用大括号不 ...
  3. 程序员的常见健康问题

    其实这些问题不仅见于程序员,其他长期经常坐在电脑前的职场人士(比如:网络编辑.站长等),都会有其中的某些健康问题.希望从事这些行业的朋友,对自己的健康问题,予以重视.以下是全文. 我最近在写<Learn Pytho ...
  4. Python 程序员的进化

    转自:http://www.codeweblog.com/news/15319/evolution-of-a-python-programmer 在综合资讯栏中看到这个帖子,觉得很有意思,转上来,最后面加上些自己的知识补 ...
  5. Zed Shaw:程序员的常见健康问题

    伯乐在线注:原文作者Zed Shaws是一位作家.软件开发人员.音乐人(下文中提到吉他手),于2010年发布<Learn Python The Hard Way>一书,他也是 Mongrel Web 服务器系 ...
  6. Python程序员鲜为人知但你应该知道的17个问题

    这篇文章主要介绍了Python程序员代码编写时应该避免的17个"坑",也可以说成Python程序员代码编写时应该避免的17个问题,需要的朋友可以参考下 一.不要使用可变对象作为函数默认值 In [1] ...
  7. 转:一个Python程序员的进化

    from: http://developer.51cto.com/art/201102/244479.htm 不久前,在互联网上出现了一篇有趣的文章,讲的是对于同一个问题,不同层次的Python程序员编出的Python代 ...
  8. 趣文:Python程序员的进化史

    #新手程序员 def factorial(x): if x == 0: return 1 else: return x * factorial(x - 1) print factorial(6) #有一年 Pascal ...
  9. 程序员的常见健康问题--摘录

    分享一篇不错的文章<Zed Shaw:程序员的常见健康问题>.程序员确实需要好好重视一下自己的身体.我从中摘录了一些内容. 程序员常见的健康问题: 腕部疼痛(重复性劳损) 盯着看移动的打印进度以及其后的句点造 ...
  10. Python 程序员经常犯的 10 个错误

    关于Python Python是一种解释性.面向对象并具有动态语义的高级程序语言.它内建了高级的数据结构,结合了动态类型和动态绑定的优点,这使得它在快速应用开发中非常有吸引力,并且可作为脚本或胶水语言来连接现有的组件或服 ...
  11. 趣文:程序员最常见的谎话

    整理编译自 Quora 上的问答贴:What are the most common lies told by programmers? 大部分答案是以下内容的变种: ○ 这个任务简单: ○ 我就快做完了: ○ 如果有 ...
  12. 程序员最常见的技术性误区

    本文来自Nalaka分享的PPT.请通过评论,分享你的想法或经验,因为我们所有人必须从我们的错误中学习. 0. 编程只是为了钱 如果你不是很喜欢编程,你的代码一定会杂乱无章.结果不仅影响你的职业生涯,你的团队成员也会跟着 ...
  13. Java程序员可能犯的3个常见SQL错误

    Java程序员不仅要具备扎实的Java编程能力,在日常的工作当中往往还要涉及到其他语言的基础知识,尤其是SQL.那么哪些常见的SQL错误是程序员们容易犯的呢?让我们一起来看看吧! 你可能看到Java程序员每周的工作是编码 ...
  14. C# 程序员易犯的 10 个错误

    关于C# C#是针对微软公共语言运行库(CLR)的开发语言之一.针对CLR的开发语言得益于如跨语言集成的性能,异常处理,安全性增强,组件交互的简化模型,调试和分析服务.对于今日的CLR来说,C#是定位到Windows桌面 ...
  15. Python超级程序员使用的开发工具

    我以个人的身份采访了几个顶尖的Python程序员,问了他们以下5个简单的问题: 当前你的主要开发任务是什么? 你在项目中使用的电脑是怎样的? 你使用什么IDE开发? 你将来的计划是什么? 有什么给Python程序员的建议 ...
  16. 使用C语言扩展Python程序的简单入门指引

    这篇文章主要介绍了使用C语言扩展Python程序的简单入门指引,来自于IBM官网网站技术文档,需要的朋友可以参考下 一.简介 Python是一门功能强大的高级脚本语言,它的强大不仅表现在其自身的功能上,而且还表现在其良好 ...
  17. 和程序员打交道的十大禁忌

    如果你正在读这篇文章,那么非常有可能是有人发给你了这条链接.这个人可能是你的朋友,同事,亲戚,父母,儿子,兄弟,表亲,姑嫂,外甥,或者恰巧帮你解决了电脑问题的一个人.他通常是程序员,系统管理员,或者 "擅长电脑 ...
  18. 程序员也要养生

    工作投身嵌入式后,从做画PCB,做验证板,写驱动,到做中间层,到架构并写一些系统,就慢慢的变得无时不刻不爬在电脑前,从07年起,我的脖子开始不舒服,夸夸响.08~09年肩膀也开始僵,11年腰酸,一个7年的老IT人深深感到 ...
  19. 为程序员量身定制的12个目标

    对程序员们来说挑战自我非常重要,要么不断创新,要么技术停滞不前.新年伊始,我整理了12个月的目标,每个目标都是对技术或个人能力的挑战,而且可以年复一年循环使用. 01. 变得有耐心 02. 保持健康 03. 拥抱变化带来 ...