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

已发表: 2022-03-11

关于 Python

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

关于这篇文章

Python 简单易学的语法可能会误导 Python 开发人员——尤其是那些刚接触该语言的开发人员——忽略它的一些微妙之处,并低估了各种 Python 语言的力量。

考虑到这一点,本文提出了一个“前 10 名”列表,其中列出了一些微妙的、难以发现的错误,这些错误甚至可能会在后面咬一些更高级的 Python 开发人员。

(注意:这篇文章的目标读者是比 Python 程序员的常见错误更高级的读者,后者更适合那些刚接触这门语言的人。)

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

Python 允许您通过为其提供默认值来指定函数参数是可选的。 虽然这是该语言的一个很棒的特性,但当默认值是mutable时,它​​可能会导致一些混乱。 例如,考虑这个 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 编程答案是函数参数的默认值仅在定义函数时计算一次。 因此, bar参数仅在首次定义foo()时才初始化为其默认值(即空列表),但随后对foo()的调用(即,未指定bar参数)将继续使用相同的列表最初初始化哪个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 Ax, Bx, Cx 1 1 1

说得通。

 >>> Bx = 2 >>> print Ax, Bx, Cx 1 2 1

是的,正如预期的那样。

 >>> Ax = 3 >>> print Ax, Bx, Cx 3 2 3

$%#!&是什么? 我们只改变了Ax 。 为什么Cx也发生了变化?

在 Python 中,类变量在内部作为字典处理,并遵循通常所说的方法解析顺序 (MRO)。 所以在上面的代码中,由于在类C中没有找到属性x ,所以会在它的基类中查找(上例中只有A ,虽然 Python 支持多重继承)。 换句话说, C没有自己的x属性,独立于A 。 因此,对Cx的引用实际上是对Ax的引用。 除非处理得当,否则这会导致 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 中, except Exception, e的语法用于将异常绑定到指定的可选第二个参数(在本例中为e ),以使其可用于进一步检查。 结果,在上面的代码中, IndexError异常并没有except语句捕获; 相反,异常最终被绑定到名为IndexError的参数。

except语句中捕获多个异常的正确方法是将第一个参数指定为包含所有要捕获的异常的元组。 此外,为了获得最大的可移植性,请使用as关键字,因为 Python 2 和 Python 3 都支持该语法:

 >>> try: ... l = ["a", "b"] ... int(l[2]) ... except (ValueError, IndexError) as e: ... pass ... >>>

常见错误 #4:误解 Python 作用域规则

Python 范围解析基于所谓的 LEGB 规则,它是L ocal、Enclosure、 G lobal、 B uilt -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已经被分配了它的 final值 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.pyb.py ,每个文件都导入另一个文件,如下所示:

a.py

 import b def f(): return bx print f()

b.py中:

 import a x = 1 def g(): print af()

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

 >>> import a 1

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

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

回到我们的示例,当我们导入a.py时,导入b.py没有问题,因为b.py不需要在导入时定义a.py中的任何内容。 b.py中对a的唯一引用是对af()的调用。 但是该调用在g()中,并且a.pyb.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 bx AttributeError: 'module' object has no attribute 'x'

哦哦。 这不好! 这里的问题是,在导入b.py的过程中,它尝试导入a.py ,这反过来又调用f() ,它试图访问bx 。 但是bx还没有被定义。 因此出现AttributeError异常。

至少有一个解决方案是非常简单的。 只需修改b.pyg()中导入a.py

 x = 1 def g(): import a # This will be evaluated only when g() is called print af()

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

 >>> import b >>> bg() 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

伊皮!

(顺便提一下,我们的 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异常。

为什么? 因为,正如这里所报告的,当解释器关闭时,模块的全局变量都设置为None 。 结果,在上面的示例中,在调用__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 的关键细微差别,例如(但不限于)本文中提出的中等高级编程问题,将有助于优化该语言的使用,同时避免一些更常见的错误。

您可能还想查看我们的 Python 面试内幕指南,以获取有关有助于识别 Python 专家的面试问题的建议。

我们希望您发现本文中的建议对您有所帮助,并欢迎您提供反馈。