Python 程序员经常犯的 10 个错误(1)


关于Python

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

关于这篇文章

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

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

 

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

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

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

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

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

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

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

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

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

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

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

考虑一下下面的例子:

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

常规用一下。

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

嗯,再试一下也一样。

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

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

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

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

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

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

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

这里出什么问题了?

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

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

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

  1. >>> lst = [123]  
  2. >>> def foo1():  
  3. ...     lst.append(5)   # 没有问题...  
  4. ...  
  5. >>> foo1()  
  6. >>> lst  
  7. [1235]  
  8.  
  9. >>> lst = [123]  
  10. >>> def foo2():  
  11. ...     lst += [5]      # ... 但是这里有问题!  
  12. ...  
  13. >>> foo2()  
  14. Traceback (most recent call last):  
  15.   File "<stdin>", line 1in <module>  
  16.   File "<stdin>", line 2in foo  
  17. 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. >>> odd = lambda x : bool(x % 2)  
  2. >>> numbers = [n for n in range(10)]  
  3. >>> for i in range(len(numbers)):  
  4. ...     if odd(numbers[i]):  
  5. ...         del numbers[i]  # BAD: Deleting item from a list while iterating over it  
  6. ...  
  7. Traceback (most recent call last):  
  8.         File "<stdin>", line 2in <module>  
  9. IndexError: list index out of range  

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

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

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

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

看下面这个例子:

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

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

  1. 0 
  2. 2 
  3. 4 
  4. 6 
  5. 8 

但实际的结果却是:

  1. 8  
  2. 8  
  3. 8  
  4. 8  
  5. 8  

惊讶吧!

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

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

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

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


评论关闭