写在前面
“人生苦短,我用python”,因为简洁灵活得到众多程序猿喜欢的面向对象计算机程序设计语言python,在使用的期间也会出现这样那样的bug,还记得在《python爬虫入门+进阶》课程中小Y老师也曾说过“写代码就是一个不断发现错误的过程”。所以,DC君搬运了进阶版的“python编程常见的十大错误”,让你在避坑这条路上姿势更漂亮一些!
1
滥用表达式作为函数参数的默认值
Python允许开发者指定函数参数的默认值,这也是Python的一大特色,但当默认值可变时,可能会给开发者带来一些困扰。例如下面定义的函数:
>>>deffoo(bar=[]):# bar is optional and defaults to []ifnot specified
... bar.append("baz")# but thislinecould be problematic, as we'll see...
... return bar
看出bug了吗?那就是在每次调用函数前没有对可变参数进行赋值,而认为该参数就是默认值。比如上面的代码,有人可能期望在反复调用foo()时返回'baz',以为每次调用foo()时,bar的值都为[],即一个空列表。
但是,让我们来看看代码运行结果:
>>> foo()
["baz"]
>>> foo()
["baz","baz"]
>>> foo()
["baz","baz","baz"]
嗯?为什么每次调用foo()后会不断把"baz"添加到已有的列表,而不是新建一个新列表呢?答案就是,函数参数的默认值仅在定义函数时执行一次。因此,仅在第一次定义foo()时,bar初始化为默认值(即空列表),此后,每次调用foo()函数时,参数bar都是第一次初始化时生成的列表。
常见的解决方案:
>>>deffoo(bar=None):
...ifbar is None:# orifnot bar:
... bar= []
... bar.append("baz")
...returnbar
...
>>> foo()
["baz"]
>>> foo()
["baz"]
>>> foo()
["baz"]
2
错误地使用类变量
代码示例:
>>>classA(object):
... x=1
...
>>>classB(A):
... pass
...
>>>classC(A):
... pass
...
>>> print A.x, B.x, C.x
1 1 1
运行结果没问题。
>>> B.x =2
>>> print A.x, B.x, C.x
121
结果也正确。
>>> A.x =3
>>> print A.x, B.x, C.x
323
什么鬼?我们只改变了A.x.,为什么C.x 也变了?
在Python中,类变量是以字典形式进行内部处理,遵循方法解析顺序(Method Resolution Order ,MRO)。因此,在上述代码中,因为在类C中没有找到属性x,它就会从父类中查找x的值(尽管Python支持多重继承,但上述代码只存在一个父类A)。换句话说,C没有独立于类A的属于自己的x。因此,C.x实际上指的是A.x。除非处理得当,否则就会导致Python出现错误。
https://www.toptal.com/python/python-class-attributes-an-overly-thorough-guide
3
错误指定异常代码块的参数
假设你有如下代码:
>>>try:
... l = ["a","b"]
...int(l[2])
... except ValueError, IndexError: # Tocatchboth exceptions, right?
... pass
...
Traceback (most recent call last):
File"", line3, in
IndexError:listindex out of range
这里的问题是except语句不接受以这种方式指定的异常列表。在Python2.x中,except Exception语句中变量e可用来把异常信息绑定到第二个可选参数上,以便进一步查看异常的情况。因此,在上述代码中,except语句并没有捕捉到IndexError异常;而是将出现的异常绑定到了参数IndexError中。
想在一个except语句同时捕捉到多个异常的正确方式是,将第一个参数指定为元组,并将要捕捉的异常类型都写入该元组中。为了方便起见,可以使用as关键字,Python 2 和Python 3都支持这种语法格式:
>>>try:
... l = ["a","b"]
...int(l[2])
... except (ValueError, IndexError) as e:
... pass
...
>>>
4
错误理解Python中变量的作用域
Python变量作用域遵循LEGB规则,LEGB是Local,Enclosing,Global,Builtin的缩写,分别代表本地作用域、封闭作用域、全局作用域和内置作用域,这个规则看起来一目了然。事实上,Python的这种工作方式较为独特,会导致一些编程错误,例如:
>>> x =10
>>> def foo():
... x +=1
... print x
...
>>> foo()
Traceback (most recent call last):
File"", line1, in
File"", line2, in foo
UnboundLocalError: local variable'x'referenced before assignment
问题出在哪?
上面的错误是因为在作用域内对变量赋值时,Python自动将该变量视为该作用域的本地变量,并对外部定义的同名变量进行了屏蔽。因此,原本正确的代码,在某个函数内部添加了一个赋值语句后,却意外收到了UnboundLocalError的报错信息。
https://docs.python.org/2/faq/programming.html#why-am-i-getting-an-unboundlocalerror-when-the-variable-has-a-value
在使用列表时,Python程序员更容易掉入此类陷阱,例如:
>>> lst = [1,2,3]
>>> def foo1():
... lst.append(5) # This works ok...
...
>>> foo1()
>>> lst
[1,2,3,5]
>>> lst = [1,2,3]
>>> def foo2():
... lst += [5] # ... butthisbombs!
...
>>> foo2()
Traceback (most recent call last):
File"", line1, in
File"", line2, in foo
UnboundLocalError: local variable'lst'referenced before assignment
奇怪,为什么foo1正常运行,而foo2崩溃了呢?
原因和上一个案例中出现的问题相似,但这里的错误更加细微。函数foo1没有对变量lst进行赋值操作,而函数foo2有赋值操作。
首先, lst += [5]是lst = lst + [5]的缩写形式,在函数foo2中试图对变量lst进行赋值操作(Python将变量lst默认为本地作用域的变量)。但是,lst += [5]语句是对lst变量自身进行的赋值操作(此时变量lst的作用域是函数foo2),但是在函数foo2中还未声明该变量,所以就报错啦!
5
在遍历列表时修改列表
下面代码中的错误很明显:
>>> odd = lambda x :bool(x %2)
>>> numbers = [nforn in range(10)]
>>>fori in range(len(numbers)):
...ifodd(numbers[i]):
... del numbers[i] # BAD: Deleting item from alistwhileiterating over it
...
Traceback (most recent call last):
File"", line2, in
IndexError:listindex out of range
有经验的程序员都知道,在Python中遍历列表或数组时不应该删除该列表(数组)中的元素。虽然上面代码的错误很明显,但是在编写复杂代码时,资深程序员也难免会犯此类错误。
幸好Python集成了大量经典的编程范式,如果运用得当,可以大大简化代码并提高编程效率。简单的代码会降低出现上述bug的几率。列表解析式(list comprehensions)就是利器之一,它将完美避开上述bug,解决方案如下:
>>> odd = lambda x :bool(x %2)
>>> numbers = [nforn in range(10)]
>>> numbers[:] = [nforn in numbersifnot odd(n)] # ahh, the beauty of it all
>>> numbers
[,2,4,6,8]
6
不理解Python闭包中的变量绑定
代码示例:
>>>defcreate_multipliers():
...return[lambda x : i * xfori inrange(5)]
>>>formultiplier increate_multipliers():
... printmultiplier(2)
...
你以为运行结果会是:
2
4
6
8
但实际输出结果是:8
8
8
8
8
惊不惊喜!
这种情况是由于Python延迟绑定(late binding)机制造成的,也就是说只有在内部函数被调用时才会搜索闭包中变量的值。所以在上述代码中,每次调用create_multipliers()函数中的return函数时,会在附近作用域中查询变量i的值。(此时,return中循环已结束,所以i值为4)。
常见解决方案:
>>>defcreate_multipliers():
...return[lambda x, i=i :i * xfori inrange(5)]
...
>>>formultiplier increate_multipliers():
... printmultiplier(2)
...
2
4
6
8
没错!我们利用了匿名函数lambda的默认参数来生成结果序列。有人觉得这种用法很简洁,有人会说它很巧妙,还有人会觉得晦涩难懂。如果你是Python开发人员,那么深刻理解上述语法对你而言非常重要。
7
模块之间出现循环依赖
假设你有两个文件,分别是a.py和b.py,两者相互导入,如下所示:
a.py模块中的代码:
importb
deff():
returnb.x
printf()
b.py模块中的代码:
importa
x =1
def g():
print a.f()
首先,我们尝试导入a.py:
>>>importa
1
运行结果正确!这似乎有点出人意料,因为我们在这里进行循环导入,应该会报错呀!
答案是,在Python中如果仅存在一个循环导入,程序不会报错。如果一个模块已经被导入,Python会自动识别而不会再次导入。但是如果每个模块试图访问其他模块不同位置的函数或变量时,那么Error又双叒叕出现了。
回到上面的示例中,当导入a.py模块时,程序可以正常导入b.py模块,因为此时b.py模块未访问a.py中定义任何的变量或函数。b.py模块仅引用了a.py模中的a.f()函数。调用的a.f()函数隶属于g()函数,而a.py或b.py模块中并没有调用g()函数。所以程序没有报错。
但是,如果我们在未导入a.py模块之前先导入b.py模块,结果会怎样?
>>>importb
Traceback(most recent call last):
File "", line 1, in
File "b.py", line 1, in
importa
File "a.py", line 6, in
printf()
File "a.py", line 4, in f
returnb.x
AttributeError: 'module' object has no attribute 'x'
报错了!问题在于,在导入b.py的过程中,它试图导入a.py模块,而a.py模块会调用f()函数,f()函数又试图访问b.x变量。但此时,还未对变量b.x进行定义,所以出现了AttributeError异常。
稍微修改下b.py,即在g()函数内部导入a.py就可以解决上述问题。
修改后的b.py:
x =1
def g():
importa #This will be evaluated only wheng()is called
print a.f()
现在我们再导入b.py模块,就不会报错啦!
>>>importb
>>> b.g()
1# Printed a first time sincemodule'a'calls'print f()'at the end
1# Printed a second time,thisone is our call to'g'
8
文件命名与Python标准库模块的名称冲突
Python的优势之一就是其集成了丰富的标准库。正因为如此,稍不留神就会在为自己的文件命名时与Python自带标准库模块重名。例如,如果你的代码中有一个名为email.py的模块,恰好就和Python标准库中email.py模块重名了。)
上述问题比较复杂。举个例子,在导入模块A的时候,假如该模块A试图导入Python标准库中的模块B,但你已经定义了一个同名模块B,模块A会错误导入你自定义的模块B,而不是Python标准库中的模块B。这种错误很糟糕,因为程序员很难察觉到是因为命名冲突而导致的。
因此,Python程序员要注意避免与Python标准库模块的命名冲突。毕竟,修改自己模块的名称比修改标准库的名称要容易的多!当然你也可以写一份Python改善建议书(Python Enhancement Proposal,PEP)提议修改标准库的名称。
9
不熟悉python2与python3之间的差异
先来看看foo.py文件中的代码:
importsys
defbar(i):
ifi==1:
raise KeyError(1)
ifi ==2:
raise ValueError(2)
def bad():
e = None
try:
bar(int(sys.argv[1]))
except KeyError as e:
print('key error')
except ValueError as e:
print('value error')
print(e)
bad()
在Python 2中,上述代码运行正常
$ python foo.py1
key error
1
$ python foo.py2
value error
2
但是在Python 3中运行时:
$ python3 foo.py1
key error
Traceback(most recent call last):
File "foo.py", line 19, in
bad()
File "foo.py", line 17, in bad
print(e)
UnboundLocalError: local variable 'e' referenced before assignment
什么情况?原来,在Python 3中,在except代码块作用域外无法访问异常对象。(原因是,Python 3会将内存堆栈中的循环引用进行保留,直到垃圾回收器运行后在内存中对其进行清理。)
https://docs.python.org/3/reference/compound_stmts.html#except
解决方法之一是,在except代码块的作用域之外,加一句异常对象的引用就可以正常访问异常对象了。下面是处理后的代码,在Python2和Python3中的运行结果一致:
importsys
defbar(i):
ifi==1:
raise KeyError(1)
ifi ==2:
raise ValueError(2)
def good():
exception = None
try:
bar(int(sys.argv[1]))
except KeyError as e:
exception = e
print('key error')
except ValueError as e:
exception = e
print('value error')
print(exception)
good()
再次在Python3中运行代码:
$ python3 foo.py1
key error
1
$ python3 foo.py2
value error
2
问题解决了!
https://www.toptal.com/python#hiring-guide
10
误用_del_方法
假设名为mod.py的文件中有如下代码:
importfoo
classBar(object):
...
def __del__(self):
foo.cleanup(self.myhandle)
然后,你想在another_mod.py文件中进行如下操作:
importmod
mybar = mod.Bar()
如果你试图运行another_mod.py,将会出现AttributeError异常。
为什么呢?因为当Python解释器关闭时,该模块的全局变量的值都会被置为None。因此,在上述示例中,在调用__del__函数时,foo的值已经为None。
https://mail.python.org/pipermail/python-bugs-list/2009-January/069209.html
调用atexit.register()函数可以解决上述的Python高阶编程问题。在调用atexit.register()函数后,当你的代码运行结束后(即正常退出程序的情况下),注册处理程序会在解释器关闭之前运行。
应用上述方法,修改后的mod.py文件如下:
importfoo
importatexit
defcleanup(handle):
foo.cleanup(handle)
classBar(object):
def __init__(self):
...
atexit.register(cleanup, self.myhandle)
当程序正常终止时,这种方法可以很方便的调用程序的清理功能。上述示例中,foo.cleanup函数会决定如何处理self.myhandle所绑定的对象,但是调用atexit.register()函数就可以由你决定何时执行清理功能。
总 结
Python是一种强大且灵活的编程语言,提供了很多编程机制和范式,它可以极大地提高我们的工作效率。但不论使用何种软件工具或编程语言,开发人员都应该彻底理解Python的语法规则和编程规范,否则将会陷入“一知半解,害已误人”的状态。
不断学习Python的语法规则,尤其文中提到这些问题,有助于降低代码的出错概率,也会提升Python编程的效率。
如果你在编程过程中也有整理一些常见错误,欢迎在留言进行讨论哟~
https://www.toptal.com/python/top-10-mistakes-that-python-programmers-make
本文经授权转载自“大数据文摘”,编译:什锦甜、Gao Ning、小鱼
感谢编译人员的付出~
领取专属 10元无门槛券
私享最新 技术干货