Python 程序员的常见错误
目录
Python 程序员常见错误笔记(翻译 Mark Lutz)。
语用错误(交互环境使用)
在交互提示符中输入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代码
给从C转向Python的程序员的提示:
- 条件测试无需括号:
if X==1:而非if (X==1): - 无需分号结尾(除非多语句同行):用
x=1; y=2; z=3时才加分号 - 不能在 while 条件中嵌入赋值:
while (x=next() != NULL)在 Python 中非法 编程错误 下面终于要讲到当你用到更多的Python的功能(数据类型,函数,模块,类等等)时可能碰到的问题了。由于篇幅有限,这里尽量精简,尤其是对一些高级的概念。要想了解更多的细节,敬请阅读Learning Python, 2nd Edition的“小贴士”以及“Gotchas”章节。 打开文件的调用不使用模块搜索路径 当你在Python中调用open()来访问一个外部的文件时,Python不会使用模块搜索路径来定位这个目标文件。它会使用你提供的绝对路径,或者假定这个文件是在当前工作目录中。模块搜索路径仅仅为模块加载服务的。 不同的类型对应的方法也不同 列表的方法是不能用在字符串上的,反之亦然。通常情况下,方法的调用是和数据类型有关的,但是内部函数通常在很多类型上都可以使用。举个例子来说,列表的reverse方法仅仅对列表有用,但是len函数对任何具有长度的对象都适用 不能直接改变不可变数据类型
不能直接修改元组、字符串等不可变对象:
T = (1, 2, 3)
T[2] = 4 # 错误:TypeError
应该通过切片和拼接重新构建:
T = T[:2] + (4,) # 正确:T 变成 (1, 2, 4)
使用简单的for循环而不是while或者range
遍历序列时优先用 for,避免 while + range 计数循环:
S = "lumberjack"
# 最优:直接迭代
for c in S:
print(c)
# 避免:用 range 计数
for i in range(len(S)):
print(S[i])
# 最差:用 while + 计数
i = 0
while i < len(S):
print(S[i])
i += 1
不要试图从那些会改变对象的函数得到结果
append() 和 sort() 直接改变对象并返回 None,不能链式赋值:
# 错误:append 返回 None
mylist = mylist.append(X) # mylist 变成 None
# 正确:直接调用,不赋值
mylist.append(X)
常见错误:在 dict 遍历中使用 sort():
# 错误:D.keys().sort() 返回 None
for k in D.keys().sort():
print(D[k])
# 正确:分开语句
keys = D.keys()
keys.sort()
for k in keys:
print(D[k])
只有在数字类型中才存在自动类型转换
数字间自动转换:
X = 123 + 3.145 # 正确:整数自动转浮点
但字符串与数字不自动转换(避免歧义):
S = “42”
I = 1
X = S + I # 错误:TypeError
# 正确:明确转换
X = int(S) + I # 相加:43
X = S + str(I) # 连接:”421”
循环的数据结构会导致循环
当对象包含自身引用时,Python 用 [...] 表示循环,避免无限递归:
L = ['grail']
L.append(L)
print(L) # ['grail', [...]]
防止意外循环:维护已访问对象列表,检测环形结构。 赋值语句不会创建对象的副本,仅创建引用
Python 赋值是引用,不是复制。修改共用引用会影响所有持有该引用的变量:
L = [1, 2, 3]
M = ['X', L, 'Y']
# M: ['X', [1, 2, 3], 'Y']
L[1] = 0
# 也改变了 M
# M: ['X', [1, 0, 3], 'Y']
要创建副本,使用切片或 copy() 方法:
L = [1, 2, 3]
M = ['X', L[:], 'Y'] # 嵌入 L 的副本
L[1] = 0
# 仅改变 L,不影响 M
# L: [1, 0, 3]
# M: ['X', [1, 2, 3], 'Y']
# 对字典使用 dict.copy()
D2 = D1.copy()
静态识别本地域的变量名
Python 在编译函数时(不是运行时)静态识别本地变量。如果函数内有赋值语句,整个函数内对该变量的所有引用都视为本地:
X = 99
def func():
print(X) # UnboundLocalError:这里 X 还未赋值
X = 88 # 此语句导致整个函数内 X 都被视为本地
func() # 出错!
要访问全局变量,用 global 关键字或模块名引用:
X = 99
def func():
global X
print(X) # 正确:99
X = 88
默认参数和可变对象
默认参数值在定义时只计算一次,所有调用都共用同一对象。修改可变默认值会影响后续调用:
def saver(x=[]):
x.append(1)
print(x)
saver([2]) # [2, 1](传入列表)
saver() # [1] (使用默认值)
saver() # [1, 1](默认值被修改了!)
saver() # [1, 1, 1](累积增长)
解决方案:用 None 作默认值,函数内创建新对象:
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()不影响from导入的名字,需重新from - 多重继承:superclass 搜索左到右,重名时左边优先
- 空 except 子句:捕捉所有异常(包括
sys.exit()),谨慎使用