Python注意事项和误区

时间:2022-02-28 01:28:25

Python是一种解释性、面向对象并具有动态语义的高级程序语言。它内建了高级的数据结构,结合了动态类型和动态绑定的优点,这使得它在快速应用开发中非常有吸引力,并且可作为脚本或胶水语言来连接现有的组件或服务。Python支持模块和包,从而鼓励了程序的模块化和代码重用。

关于这篇文章

Python简单易学的语法可能会使Python开发者–尤其是那些编程的初学者–忽视了它的一些微妙的地方并低估了这门语言的能力。

有鉴于此,本文列出了一个“10强”名单,枚举了甚至是高级Python开发人员有时也难以捕捉的错误。

常见错误 1: 滥用表达式作为函数参数的默认值

Python允许为函数的参数提供默认的可选值。尽管这是语言的一大特色,但是它可能会导致一些易变默认值的混乱。例如,看一下这个Python函数的定义:

1
2
3
>>>  def  foo(bar = []):         # bar is optional and defaults to [] if not specified  
...    bar.append( "baz" )     # but this line could be problematic, as we'll see...  
...     return  bar

一个常见的错误是认为在函数每次不提供可选参数调用时可选参数将设置为默认指定值。在上面的代码中,例如,人们可能会希望反复(即不明确指定bar参数)地调用foo()时总返回'baz',由于每次foo()调用时都假定(不设定bar参数)bar被设置为[](即一个空列表)。

但是让我们看一下这样做时究竟会发生什么:

1
2
3
4
>>> foo()  
[ "baz" ]>>> foo()  
[ "baz" ,  "baz" ]>>> foo()  
[ "baz" ,  "baz" ,  "baz" ]

耶?为什么每次foo()调用时都要把默认值"baz"追加到现有列表中而不是创建一个新的列表呢?

答案是函数参数的默认值只会评估使用一次—在函数定义的时候。因此,bar参数在初始化时为其默认值(即一个空列表),即foo()首次定义的时候,但当调用foo()时(即,不指定bar参数时)将继续使用bar原本已经初始化的参数。

下面是一个常见的解决方法:

1
2
3
4
5
6
7
8
9
10
11
12
>>>  def  foo(bar = None ):  
...     if  bar  is  None :         # or if not bar:  
...        bar  =  []  
...    bar.append( "baz" )  
...     return  bar  
...  
>>> foo()  
[ "baz" ]  
>>> foo()  
[ "baz" ]  
>>> foo()  
[ "baz" ]

常见错误 2: 错误地使用类变量

考虑一下下面的例子:

1
2
3
4
5
6
7
8
9
10
11
>>>  class  A( object ):  
...     x  =  1 
...  
>>>  class  B(A):  
...      pass 
...  
>>>  class  C(A):  
...      pass 
...  
>>>  print  A.x, B.x, C.x  
1  1  1

常规用一下。

1
2
3
>>> B.x  =  2 
>>>  print  A.x, B.x, C.x  
1  2  1

嗯,再试一下也一样。

1
2
3
>>> A.x  =  3 
>>>  print  A.x, B.x, C.x  
3  2  3

什么 $%#!&?? 我们只改了A.x,为什么C.x也改了?

在Python中,类变量在内部当做字典来处理,其遵循常被引用的方法解析顺序(MRO)。所以在上面的代码中,由于class C中的x属性没有找到,它会向上找它的基类(尽管Python支持多重继承,但上面的例子中只有A)。换句话说,class C中没有它自己的x属性,其独立于A。因此,C.x事实上是A.x的引用。

常见错误 3: 为 except 指定错误的参数

假设你有如下一段代码:

1
2
3
4
5
6
7
8
9
>>>  try :  
...     l  =  [ "a" "b" ]  
...      int (l[ 2 ])  
...  except  ValueError, IndexError:   # To catch both exceptions, right?  
...      pass 
...  
Traceback (most recent call last):  
   File  "<stdin>" , line  3 in  <module>  
IndexError:  list  index out of  range

这里的问题在于 except 语句并不接受以这种方式指定的异常列表。相反,在Python 2.x中,使用语法 except Exception, e 是将一个异常对象绑定到第二个可选参数(在这个例子中是 e)上,以便在后面使用。所以,在上面这个例子中,IndexError 这个异常并不是被except语句捕捉到的,而是被绑定到一个名叫 IndexError的参数上时引发的。

在一个except语句中捕获多个异常的正确做法是将第一个参数指定为一个含有所有要捕获异常的元组。并且,为了代码的可移植性,要使用as关键词,因为Python 2 和Python 3都支持这种语法:

1
2
3
4
5
6
7
>>>  try :  
...     l  =  [ "a" "b" ]  
...      int (l[ 2 ])  
...  except  (ValueError, IndexError) as e:    
...      pass 
...  
>>>

常见错误 4:  不理解Python的作用域

Python是基于 LEGB 来进行作用于解析的, LEGB 是 Local, Enclosing, Global, Built-in 的缩写。看起来“见文知意”,对吗?实际上,在Python中还有一些需要注意的地方,先看下面一段代码:

1
2
3
4
5
6
7
8
9
10
>>> x  =  10 
>>>  def  foo():  
...     x  + =  1 
...      print  x  
...  
>>> foo()  
Traceback (most recent call last):  
   File  "<stdin>" , line  1 in  <module>  
   File  "<stdin>" , line  2 in  foo  
UnboundLocalError: local variable  'x'  referenced before assignment

这里出什么问题了?

上面的问题之所以会发生是因为当你给作用域中的一个变量赋值时,Python 会自动的把它当做是当前作用域的局部变量,从而会隐藏外部作用域中的同名变量。

很多人会感到很吃惊,当他们给之前可以正常运行的代码的函数体的某个地方添加了一句赋值语句之后就得到了一个 UnboundLocalError 的错误。  (你可以在这里了解到更多)

尤其是当开发者使用 lists 时,这个问题就更加常见.  请看下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> lst  =  [ 1 2 3 ]  
>>>  def  foo1():  
...     lst.append( 5 )    # 没有问题...  
...  
>>> foo1()  
>>> lst  
[ 1 2 3 5 ]  
  
>>> lst  =  [ 1 2 3 ]  
>>>  def  foo2():  
...     lst  + =  [ 5 ]       # ... 但是这里有问题!  
...  
>>> foo2()

 

Traceback (most recent call last):  

  File "<stdin>", line 1, in <module>  

  File "<stdin>", line 2, in foo  

UnboundLocalError: local variable 'lst' referenced before assignment 

嗯?为什么 foo2 报错,而foo1没有问题呢?

原因和之前那个例子的一样,不过更加令人难以捉摸。foo1 没有对 lst 进行赋值操作,而 foo2 做了。要知道, lst += [5] 是 lst = lst + [5] 的缩写,我们试图对 lst 进行赋值操作(Python把他当成了局部变量)。此外,我们对 lst 进行的赋值操作是基于 lst 自身(这再一次被Python当成了局部变量),但此时还未定义。因此出错!

常见错误 5:当迭代时修改一个列表(List)

下面代码中的问题应该是相当明显的:

1
2
3
4
5
6
>>> odd  =  lambda  x :  bool (x  %  2 )  
>>> numbers  =  [n  for  in  range ( 10 )]  
>>>  for  in  range ( len (numbers)):  
...      if  odd(numbers[i]):  
...          del  numbers[i]   # BAD: Deleting item from a list while iterating over it  
...

Traceback (most recent call last):  

        File "<stdin>", line 2, in <module>  

IndexError: list index out of range  

当迭代的时候,从一个 列表 (List)或者数组中删除元素,对于任何有经验的开发者来说,这是一个众所周知的错误。尽管上面的例子非常明显,但是许多高级开发者在更复杂的代码中也并非是故意而为之的。

幸运的是,Python包含大量简洁优雅的编程范例,若使用得当,能大大简化和精炼代码。这样的好处是能得到更简化和更精简的代码,能更好的避免程序中出现当迭代时修改一个列表(List)这样的bug。一个这样的范例是递推式列表(list comprehensions)。而且,递推式列表(list comprehensions)针对这个问题是特别有用的,通过更改上文中的实现,得到一段极佳的代码:

1
2
3
4
5
>>> odd  =  lambda  x :  bool (x  %  2 )  
>>> numbers  =  [n  for  in  range ( 10 )]  
>>> numbers[:]  =  [n  for  in  numbers  if  not  odd(n)]   # ahh, the beauty of it all  
>>> numbers  
[ 0 2 4 6 8 ]

常见错误 6: 不明白Python在闭包中是如何绑定变量的

看下面这个例子:

1
2
3
4
5
>>>  def  create_multipliers():  
...      return  [ lambda  x : i  *  for  in  range ( 5 )]  
>>>  for  multiplier  in  create_multipliers():  
...      print  multiplier( 2 )  
...

你也许希望获得下面的输出结果:

但实际的结果却是:

8  

8  

8  

8  

8  

惊讶吧!

这之所以会发生是由于Python中的“后期绑定”行为——闭包中用到的变量只有在函数被调用的时候才会被赋值。所以,在上面的代码中,任何时候,当返回的函数被调用时,Python会在该函数被调用时的作用域中查找 i 对应的值(这时,循环已经结束,所以 i 被赋上了最终的值——4)。

解决的方法有一点hack的味道:

1
2
3
4
5
6
>>>  def  create_multipliers():  
...      return  [ lambda  x, i = i : i  *  for  in  range ( 5 )]  
...  
>>>  for  multiplier  in  create_multipliers():  
...      print  multiplier( 2 )  
...

在这里,我们利用了默认参数来生成一个匿名的函数以便实现我们想要的结果。有人说这个方法很巧妙,有人说它难以理解,还有人讨厌这种做法。但是,如果你是一个 Python 开发者,理解这种行为很重要。

你定义一个函数,函数内的变量并不是立刻就把值绑定了,而是等调用的时候再查找这个变量,如图,定义函数的时候没有 foo 变量,但是仍然可以,只要调用的时候环境里有就行。

一个道理,在 for 里面 i 的值是不断改写的,但是 lambda 里面只是储存了 i 的符号,调用的时候再查找。这就是你说的后期绑定。

为什么你加了默认参数就成功了呢?因为在创建函数的时候就要获取默认参数的值,放到 lambda 的环境中,所以这里相当于存在一个赋值,从而 lambda 函数环境中有了一个独立的 i。

最后,优雅的写法是用生成器:
for multiplier in (lambda x : i * x for i in range(5)):
print(multiplier(2))
这样惰性求值就可以避免 i 的改写。
或者:
def create_multipliers():
for i in range(5):
yield lambda x: i * x

for multiplier in create_multipliers():
print(multiplier(2))

常见错误 7: 创建循环依赖模块

让我们假设你有两个文件,a.py 和 b.py,他们之间相互引用,如下所示:

a.py:

1
2
3
4
import  b  
def  f():  
     return  b.x    
print  f()

b.py:

1
2
3
4
import  a  
=  1 
def  g():  
     print  a.f()

首先,让我们尝试引入 a.py:

>>> import a  

可以正常工作。这也许是你感到很奇怪。毕竟,我们确实在这里引入了一个循环依赖的模块,我们推测这样会出问题的,不是吗?

答案就是在Python中,仅仅引入一个循环依赖的模块是没有问题的。如果一个模块已经被引入了,Python并不会去再次引入它。但是,根据每个模块要访问其他模块中的函数和变量位置的不同,就很可能会遇到问题。

所以,回到我们这个例子,当我们引入 a.py 时,再引入 b.py 不会产生任何问题,因为当引入的时候,b.py 不需要 a.py 中定义任何东西。b.py 中唯一引用 a.py 中的东西是调用 a.f()。 但是那个调用是发生在g() 中的,并且 a.py 和 b.py 中都没有调用 g()。所以运行正常。

但是,如果我们尝试去引入b.py 会发生什么呢?(在这之前不引入a.py),如下所示:

1
>>>  import  b

Traceback (most recent call last):  

        File "<stdin>", line 1, in <module>  

        File "b.py", line 1, in <module>  

    import a  

        File "a.py", line 6, in <module>  

    print f()  

        File "a.py", line 4, in f  

    return b.x  

AttributeError: 'module' object has no attribute 'x' 

啊哦。 出问题了!此处的问题是,在引入b.py的过程中,Python尝试去引入 a.py,但是a.py 要调用f(),而f() 有尝试去访问 b.x。但是此时 b.x 还没有被定义呢。所以发生了 AttributeError 异常。

至少,解决这个问题很简单,只需修改b.py,使其在g()中引入 a.py:

1
2
3
4
=  1 
def  g():  
     import  a     # 只有当g()被调用的时候才会引入a  
     print  a.f()

现在,当我们再引入b,没有任何问题:

1
2
3
4
>>>  import  b  
>>> b.g()  
1     # Printed a first time since module 'a' calls 'print f()' at the end  
1     # Printed a second time, this one is our call to 'g'
1
常见错误  8 : 与Python标准库中的模块命名冲突

Python一个令人称赞的地方是它有丰富的模块可供我们“开箱即用”。但是,如果你没有有意识的注意的话,就很容易出现你写的模块和Python自带的标准库的模块之间发生命名冲突的问题(如,你也许有一个叫 email.py 的模块,但这会和标准库中的同名模块冲突)。

这可能会导致很怪的问题,例如,你引入了另一个模块,但这个模块要引入一个Python标准库中的模块,由于你定义了一个同名的模块,就会使该模块错误的引入了你的模块,而不是 stdlib 中的模块。这就会出问题了。

因此,我们必须要注意这个问题,以避免使用和Python标准库中相同的模块名。修改你包中的模块名要比通过 Python Enhancement Proposal (PEP) 给Python提建议来修改标准库的模块名容易多了。

常见错误 #9: 未能解决Python 2和Python 3之间的差异

请看下面这个 filefoo.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import  sys  
def  bar(i):  
     if  = =  1 :  
         raise  KeyError( 1 )  
     if  = =  2 :  
         raise  ValueError( 2 )  
  
def  bad():  
     =  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.py 1 

key error  

$ python foo.py 2 

value error  

但是,现在让我们把它在Python 3中运行一下:

$ python3 foo.py 1 

key error  

Traceback (most recent call last):  

  File "foo.py", line 19, in <module>  

    bad()  

  File "foo.py", line 17, in bad  

    print(e)  

UnboundLocalError: local variable 'e' referenced before assignment  

出什么问题了? “问题”就是,在 Python 3 中,异常的对象在 except 代码块之外是不可见的。(这样做的原因是,它将保存一个对内存中堆栈帧的引用周期,直到垃圾回收器运行并且从内存中清除掉引用。了解更多技术细节请参考这里) 。

一种解决办法是在 except 代码块的外部作用域中定义一个对异常对象的引用,以便访问。下面的例子使用了该方法,因此最后的代码可以在Python 2 和 Python 3中运行良好。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import  sys  
def  bar(i):  
     if  = =  1 :  
         raise  KeyError( 1 )  
     if  = =  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()

在Py3k中运行:

$ python3 foo.py 1 

key error  

$ python3 foo.py 2 

value error  

正常!

(顺便提一下, 我们的 Python Hiring Guide 讨论了当我们把代码从Python 2 迁移到 Python 3时的其他一些需要知道的重要差异。)

常见错误 10: 误用__del__方法

假设你有一个名为 calledmod.py 的文件:

1
2
3
4
5
import  foo  
class  Bar( object ):  
            ...  
     def  __del__( self ):  
         foo.cleanup( self .myhandle)

并且有一个名为 another_mod.py 的文件:

import mod  

mybar = mod.Bar()  

你会得到一个 AttributeError 的异常。

为什么呢?因为,正如这里所说,当解释器退出的时候,模块中的全局变量都被设置成了 None。所以,在上面这个例子中,当 __del__ 被调用时,foo 已经被设置成了None。

解决方法是使用 atexit.register() 代替。用这种方式,当你的程序结束执行时(意思是正常退出),你注册的处理程序会在解释器退出之前执行。

了解了这些,我们可以将上面 mod.py 的代码修改成下面的这样:

1
2
3
4
5
6
7
8
import  foo  
import  atexit  
def  cleanup(handle):  
     foo.cleanup(handle)  
class  Bar( object ):  
     def  __init__( self ):  
         ...  
         atexit.register(cleanup,  self .myhandle)

这种实现方式提供了一个整洁并且可信赖的方法用来在程序退出之前做一些清理工作。很显然,它是由foo.cleanup 来决定对绑定在 self.myhandle 上对象做些什么处理工作的,但是这就是你想要的。

总结

Python是一门强大的并且很灵活的语言,它有很多机制和语言规范来显著的提高你的生产力。和其他任何一门语言或软件一样,如果对它能力的了解有限,这很可能会给你带来阻碍,而不是好处。正如一句谚语所说的那样 “knowing enough to be dangerous”(译者注:意思是自以为已经了解足够了,可以做某事了,但其实不是)。

熟悉Python的一些关键的细微之处,像本文中所提到的那些(但不限于这些),可以帮助我们更好的去使用语言,从而避免一些常见的陷阱。

问题一:以下的代码的输出将是什么? 说出你的答案并解释。

答案

以上代码的输出是:

使你困惑或是惊奇的是关于最后一行的输出是 3 2 3 而不是 3 2 1。为什么改变了 Parent.x的值还会改变 Child2.x 的值,但是同时 Child1.x 值却没有改变?

这个答案的关键是,在 Python 中,类变量在内部是作为字典处理的。如果一个变量的名字没有在当前类的字典中发现,将搜索祖先类(比如父类)直到被引用的变量名被找到(如果这个被引用的变量名既没有在自己所在的类又没有在祖先类中找到,会引发一个 AttributeError 异常 )。

因此,在父类中设置 x = 1 会使得类变量 X 在引用该类和其任何子类中的值为 1。这就是因为第一个 print 语句的输出是 1 1 1

随后,如果任何它的子类重写了该值(例如,我们执行语句 Child1.x = 2),然后,该值仅仅在子类中被改变。这就是为什么第二个 print 语句的输出是 1 2 1

最后,如果该值在父类中被改变(例如,我们执行语句 Parent.x = 3),这个改变会影响到任何未重写该值的子类当中的值(在这个示例中被影响的子类是 Child2)。这就是为什么第三个 print 输出是 3 2 3

问题二:以下的代码的输出将是什么? 说出你的答案并解释?

答案

这个答案实际依赖于你使用的是 Python 2 还是 Python 3。

在 Python 3 中,期望的输出是:

在 Python 2 中,尽管如此,以上代码的输出将是:

默认,如果两个操作数都是整数,Python 2 自动执行整型计算。结果,5/2 值为 2,然而 5./2 值为 2.5

注意,尽管如此,你可以在 Python 2 中重载这一行为(比如达到你想在 Python 3 中的同样结果),通过添加以下导入:

也需要注意的是“双划线”(//)操作符将一直执行整除,而不管操作数的类型,这就是为什么 5.0//2.0 值为 2.0

注: 在 Python 3 中,/ 操作符是做浮点除法,而 // 是做整除(即商没有余数,比如 10 // 3 其结果就为 3,余数会被截除掉,而 (-7) // 3 的结果却是 -3。这个算法与其它很多编程语言不一样,需要注意,它们的整除运算会向0的方向取值。而在 Python 2 中,/ 就是整除,即和 Python 3 中的 // 操作符一样,)

问题三:以下代码将输出什么?

答案

以上代码将输出 [],并且不会导致一个 IndexError

正如人们所期望的,试图访问一个超过列表索引值的成员将导致 IndexError(比如访问以上列表的 list[10])。尽管如此,试图访问一个列表的以超出列表成员数作为开始索引的切片将不会导致 IndexError,并且将仅仅返回一个空列表。

一个讨厌的小问题是它会导致出现 bug ,并且这个问题是难以追踪的,因为它在运行时不会引发错误。

问题四:以下的代码的输出将是什么? 说出你的答案并解释?

你将如何修改 multipliers 的定义来产生期望的结果

答案

以上代码的输出是 [6, 6, 6, 6] (而不是 [0, 2, 4, 6])。

这个的原因是 Python 的闭包的后期绑定导致的 late binding,这意味着在闭包中的变量是在内部函数被调用的时候被查找。所以结果是,当任何 multipliers() 返回的函数被调用,在那时,i 的值是在它被调用时的周围作用域中查找,到那时,无论哪个返回的函数被调用,for 循环都已经完成了,i 最后的值是 3,因此,每个返回的函数 multiplies 的值都是 3。因此一个等于 2 的值被传递进以上代码,它们将返回一个值 6 (比如: 3 x 2)。

(顺便说下,正如在 The Hitchhiker’s Guide to Python 中指出的,这里有一点普遍的误解,是关于 lambda 表达式的一些东西。一个 lambda 表达式创建的函数不是特殊的,和使用一个普通的 def 创建的函数展示的表现是一样的。)

这里有两种方法解决这个问题。

最普遍的解决方案是创建一个闭包,通过使用默认参数立即绑定它的参数。例如:

另外一个选择是,你可以使用 functools.partial 函数:

问题五:以下的代码的输出将是什么? 说出你的答案并解释?

你将如何修改 extendList 的定义来产生期望的结果

以上代码的输出为:

许多人会错误的认为 list1 应该等于 [10] 以及 list3 应该等于 ['a']。认为 list 的参数会在 extendList 每次被调用的时候会被设置成它的默认值 []

尽管如此,实际发生的事情是,新的默认列表仅仅只在函数被定义时创建一次。随后当 extendList 没有被指定的列表参数调用的时候,其使用的是同一个列表。这就是为什么当函数被定义的时候,表达式是用默认参数被计算,而不是它被调用的时候。

因此,list1 和 list3 是操作的相同的列表。而 list2是操作的它创建的独立的列表(通过传递它自己的空列表作为list 参数的值)。

extendList 函数的定义可以做如下修改,但,当没有新的 list 参数被指定的时候,会总是开始一个新列表,这更加可能是一直期望的行为。


使用这个改进的实现,输出将是: