个性化阅读
专注于IT技术分析

Buggy Python代码:Python开发人员最常犯的10个错误

本文概述

关于Python

Python是一种具有动态语义的解释性, 面向对象的高级编程语言。它的高级内置数据结构与动态类型和动态绑定相结合, 使其对于快速应用程序开发以及用作脚本或连接现有组件或服务的粘合语言非常有吸引力。 Python支持模块和包, 从而鼓励程序模块化和代码重用。

关于本文

Python的简单易学语法可能会误导Python开发人员(尤其是那些刚接触该语言的开发人员), 从而错过了它的一些细微之处, 并低估了各种Python语言的功能。

考虑到这一点, 本文提出了一个”前十名”清单, 列出了一些细微的, 难以捕捉的错误, 这些错误甚至可能咬住后面的一些更高级的Python开发人员。

(注意:本文的读者对象是比Python程序员的常见错误更高级的用户, 后者更适合于对Python语言较新的用户。)

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

Python允许你通过提供默认值来指定函数参数是可选的。尽管这是该语言的重要功能, 但当默认值可变时, 可能会引起混乱。例如, 考虑以下Python函数定义:

>>> 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

一个常见的错误是认为每次调用函数时都会将可选参数设置为指定的默认表达式, 而不为可选参数提供值。例如, 在上面的代码中, 可能希望重复调用foo()(即, 不指定bar参数)将始终返回” baz”, 因为假设是每次调用foo()(不带bar) bar参数设置为[](即一个新的空列表)。

但是, 让我们看看执行此操作时实际发生的情况:

>>> foo()
["baz"]
>>> foo()
["baz", "baz"]
>>> foo()
["baz", "baz", "baz"]

??为什么每次调用foo()时都将默认值” baz”追加到现有列表中, 而不是每次都创建一个新列表?

更高级的Python编程答案是, 在定义函数时, 函数参数的默认值仅计算一次。因此, 只有在首次定义foo()时, bar参数才会初始化为其默认值(即, 空列表), 但随后对foo()的调用(即, 未指定bar参数)将继续使用相同的列表最初初始化哪个条。

仅供参考, 针对此问题的常见解决方法如下:

>>> 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:错误使用类变量

考虑以下示例:

>>> class A(object):
...     x = 1
...
>>> class B(A):
...     pass
...
>>> class C(A):
...     pass
...
>>> print A.x, B.x, C.x
1 1 1

说得通。

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

是的, 再次如预期。

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

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

在Python中, 类变量在内部作为字典处理, 并遵循通常称为”方法解析顺序(MRO)”的方法。因此, 在上面的代码中, 由于在类C中找不到属性x, 因此将在其基类中查找该属性(虽然Python支持多种继承, 但在上面的示例中仅为A)。换句话说, C没有独立于A的自己的x属性。因此, 对C.x的引用实际上是对A.x的引用。除非正确处理, 否则会导致Python问题。了解有关Python中类属性的更多信息。

常见错误#3:错误地为异常块指定参数

假设你有以下代码:

>>> 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中, 使用Exception(例外)e以外的语法将例外绑定到指定的可选第二个参数(在本例中为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规则, 它是Local, Enclosing, Global, Built-in的简写。看起来很简单, 对吧?好吧, 实际上, 它在Python中的工作方式有些细微之处, 这使我们想到了下面常见的更高级的Python编程问题。考虑以下:

>>> 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。 (你可以在此处了解更多信息。)

在使用列表时, 这常常使开发人员绊倒。考虑以下示例:

>>> 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]      # ... but this bombs!
...
>>> 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

??为什么在foo1运行良好的情况下foo2会炸弹?

答案与先前示例问题中的答案相同, 但是公认的是更微妙的。 foo1没有分配给lst, 而foo2是。记住lst + = [5]实际上只是lst = lst + [5]的简写, 我们看到我们正在尝试为lst分配一个值(因此被Python假定在本地范围内)。但是, 我们希望分配给lst的值基于lst本身(再次假定现在位于本地范围内), 但尚未定义。繁荣。

常见错误#5:在迭代列表时修改列表

以下代码的问题应该非常明显:

>>> odd = lambda x : bool(x % 2)
>>> numbers = [n for n in range(10)]
>>> for i 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

迭代时从列表或数组中删除项目是任何有经验的软件开发人员都熟知的Python问题。但是, 尽管上面的示例可能很明显, 但即使是高级开发人员, 也可能会在更复杂的代码中无意间将其咬住。

幸运的是, Python包含了许多优雅的编程范例, 如果使用得当, 它们可以大大简化和简化代码。这样做的附带好处是, 在迭代过程中意外删除列表项时, 更不可能被更简单的代码咬住。这样的范例之一就是列表理解。此外, 列表推导对于避免此特定问题特别有用, 如上述代码的替代实现所示, 该实现非常有效:

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

常见错误#6:混淆Python如何在闭包中绑定变量

考虑以下示例:

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

你可能期望以下输出:

0
2
4
6
8

但是你实际上得到:

8
8
8
8
8

惊喜!

发生这种情况的原因是Python的后期绑定行为, 该行为表示在调用内部函数时会查询闭包中使用的变量的值。因此, 在上面的代码中, 无论何时调用任何返回的函数, 在调用i时都会在周围的范围中查找i的值(到那时, 循环已完成, 因此我已经为其分配了最终值)值4)。

这个常见的Python问题的解决方案有点麻烦:

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

瞧!我们在这里利用默认参数来生成匿名函数, 以实现所需的行为。有些人会称之为优雅。有人会称之为微妙。有些讨厌。但是, 如果你是Python开发人员, 那么无论如何都要了解这一点很重要。

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

假设你有两个文件a.py和b.py, 每个文件都导入另一个文件, 如下所示:

在a.py中:

import b

def f():
    return b.x
	
print f()

在b.py中:

import a

x = 1

def g():
    print a.f()

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

>>> import a
1

工作得很好。也许这让你感到惊讶。毕竟, 我们在这里确实有一个循环导入, 大概应该是一个问题, 不是吗?

答案是, 循环导入的存在本身并不是Python中的问题。如果已经导入了模块, 那么Python足够聪明, 不会尝试重新导入它。但是, 根据每个模块试图访问另一个模块中定义的函数或变量的不同点, 你的确可能会遇到问题。

因此, 回到我们的示例, 当我们导入a.py时, 导入b.py毫无问题, 因为b.py不需要在导入时就定义a.py中的任何内容。 b.py中对a的唯一引用是对a.f()的调用。但是该调用在g()中, 而a.py或b.py中的任何调用都不会调用g()。所以生活是美好的。

但是, 如果我们尝试导入b.py(即先前没有导入a.py), 会发生什么:

>>> 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的过程中, 它尝试导入a.py, 而后者又调用f(), 后者尝试访问b.x。但是b.x尚未定义。因此, AttributeError异常。

至少有一个解决方案很简单。只需修改b.py即可在g()中导入a.py:

x = 1

def g():
    import a	# This will be evaluated only when g() is called
    print a.f()

否, 当我们导入它时, 一切都很好:

>>> 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'

常见错误#8:名称与Python标准库模块冲突

Python的优点之一是”开箱即用”附带的丰富的库模块。但是结果是, 如果你不自觉地避免使用它, 那么在其中一个模块的名称与Python随附的标准库中具有相同名称的模块之间发生名称冲突就不那么困难了(例如, 你的代码中可能会有一个名为email.py的模块, 这将与同名的标准库模块发生冲突)。

这可能会导致棘手的问题, 例如导入另一个库, 而该库又试图导入模块的Python标准库版本, 但是由于你具有相同名称的模块, 因此另一个包错误地导入了你的版本, 而不是其中的版本Python标准库。这是发生严重Python错误的地方。

因此, 应注意避免使用与Python标准库模块中的名称相同的名称。与提交Python增强提案(PEP)以便在上游请求名称更改并尝试获得批准相比, 更改软件包中模块的名称要容易得多。

常见错误9:无法解决Python 2和Python 3之间的差异

考虑以下文件foo.py:

import sys

def bar(i):
    if i == 1:
        raise KeyError(1)
    if i == 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.py 1
key error
1
$ python foo.py 2
value error
2

但是现在让我们来看看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都友好的代码:

import sys

def bar(i):
    if i == 1:
        raise KeyError(1)
    if i == 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
1
$ python3 foo.py 2
value error
2

pp!

(顺便提一句, 我们的《 Python招聘指南》讨论了将代码从Python 2迁移到Python 3时需要注意的许多其他重要区别。)

常见错误10:滥用__del__方法

假设你在名为mod.py的文件中有此文件:

import foo

class Bar(object):
   	    ...
    def __del__(self):
        foo.cleanup(self.myhandle)

然后, 你尝试从another_mod.py执行此操作:

import mod
mybar = mod.Bar()

你会看到一个丑陋的AttributeError异常。

为什么?因为, 如此处报告的那样, 当解释器关闭时, 模块的全局变量都设置为”无”。结果, 在上面的示例中, 在调用__del__时, 名称foo已被设置为None。

解决此高级Python编程问题的方法是改用atexit.register()。这样, 当你的程序完成执行时(即正常退出时), 你的注册处理程序将在解释器关闭之前启动。

有了这样的理解, 上面的mod.py代码的修复程序可能看起来像这样:

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是一种功能强大且灵活的语言, 具有许多可以极大地提高生产率的机制和范例。但是, 就像使用任何软件工具或语言一样, 对其功能的有限了解或欣赏有时可能更多的是障碍而不是收益, 而使人们处于”知道足够危险”的谚语状态。

熟悉Python的关键细节, 例如(但不限于)本文提出的中度高级的编程问题, 将有助于优化语言的使用, 同时避免一些更常见的错误。

你可能还想查看我们的《 Insider的Python面试指南》, 以获取有关面试问题的建议, 这些问题可以帮助你确定Python专家。

希望本文对你有所帮助, 并欢迎你提供反馈。

赞(0)
未经允许不得转载:srcmini » Buggy Python代码:Python开发人员最常犯的10个错误

评论 抢沙发

评论前必须登录!