作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
马丁Chikilian的头像

马丁Chikilian

Martin是一名全栈工程师,自2007年以来一直担任专业Python开发人员.

Share

关于Python

Python 是一种解释的、面向对象的、具有动态语义的高级编程语言吗. 它的高级内置数据结构, 结合了动态类型和动态绑定, 让它对…有吸引力 快速应用开发,以及用作脚本语言或粘合语言来连接现有组件或服务. Python支持模块和包,因此鼓励程序模块化和代码重用.

关于本文

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

考虑到这一点, 这篇文章给出了一个“十大”名单,有些微妙, 很难捕捉, 还有一些常见的Python错误,这些错误可能会更严重 高级Python开发人员 在后面.

(注:本文的目标读者是高级读者 Python程序员的常见错误, 它更适合那些刚接触Python语言,可能不太熟悉常见Python错误的人.)

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

Python允许您指定函数参数为 可选 通过提供 默认值 for it. 虽然这是该语言的一个伟大特性, 当默认值为时,可能会导致一些混淆 可变的. 例如,考虑以下Python函数定义:

>>> def foo(bar=[]):        # bar is 可选 and defaults to [] if not specified
...    bar.追加("baz") #,但这一行可能会有问题,我们将看到...
...    返回酒吧

一个常见的错误是认为可选参数将被设置为指定的默认表达式 每一次 在不为可选参数提供值的情况下调用函数. 例如,在上面的代码中,人们可能期望调用 foo() 多次(我.e.,而不指定 bar 参数)总是返回 'baz',因为假设是这样的 每一次 foo() 叫做(没有? bar 参数指定) bar 设为 [] (i.e.,一个新的空列表).

但是让我们看看当你这样做的时候会发生什么:

>>> foo()
["记者"]
>>> foo()
(“记者”,“巴兹”)
>>> foo()
["baz", "baz", "baz"]

Huh? 为什么它一直附加的默认值 "baz" to an 现有的 每次列出 foo() 调用,而不是创建 new 每次列出?

更高级的Python编程答案是 函数参数的默认值只计算一次, 在函数被定义的时候. 因此, bar 参数初始化为其默认值(i.e.(一个空列表),只有当 foo() 先定义,然后调用 foo() (i.e.,没有。 bar 参数指定)将继续使用相同的列表 bar 是初始化的.

仅供参考,一个常见的解决方案如下:

>>> def foo(bar=None):
...    如果bar为None:		# or if not bar:
...        Bar = []
...    bar.追加(“巴兹”)
...    返回酒吧
...
>>> foo()
["记者"]
>>> foo()
["记者"]
>>> foo()
["记者"]

常见错误#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). 所以在上面的代码中,既然属性 x 在课堂上找不到吗 C,它将在其基类(仅)中查找 A 在上面的例子中,尽管Python支持多重继承). 换句话说, C 没有自己的 x 属性,独立于 A. 因此,对 C.x 实际上是指什么 A.x. 除非处理得当,否则会导致Python问题. 了解更多关于 Python中的类属性.

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

假设您有以下代码:

>>> try:
...     L = ["a", "b"]
...     int (l [2])
... 除了ValueError, IndexError: #捕获两个异常,对吧?
...     pass
...
回溯(最近一次调用):
  File "", line 3, in 
IndexError:列表索引超出范围

这里的问题是 除了 声明并 not 获取以这种方式指定的异常列表. 相反,在Python 2中.X,语法 除了例外,e 将异常绑定到 可选 指定的第二个参数(在本例中) e),以便作进一步检查. 因此,在上面的代码中 IndexError 例外是 not 被警察抓住 除了 statement; rather, the 异常 而不是 ends up being bound to a parameter named IndexError. 像这样的Python代码错误很常见.

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

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

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

Python作用域解析是基于所谓的 LEGB 规则,也就是 Local, Enclosing, Global, Built-in. 看起来很简单,对吧? Well, 实际上, 这在Python中的工作方式有一些微妙之处, 这就引出了下面常见的更高级的Python编程问题. 考虑以下几点:

>>> x = 10
>>> def foo():
...     x += 1
...     打印x
...
>>> foo()
回溯(最近一次调用):
  File "", line 1, in 
  File "", line 2, in foo
UnboundLocalError:赋值前引用的局部变量“x”

有什么问题吗??

发生上述错误的原因是,当您执行 赋值 对于作用域中的变量, 该变量被Python自动认为是该作用域的局部变量 并在任何外部作用域中隐藏任何类似命名的变量.

因此,许多人对获得一份工作感到惊讶 UnboundLocalError 在以前的工作代码中,通过在函数体的某处添加赋值语句来修改它. (你可以阅读更多 here.)

开发人员在使用时遇到这种情况尤其常见 lists. 考虑下面的例子:

>>> lst = [1, 2, 3]
>>> def foo1():
...     lst.#这个工作正常...
...
>>> foo1()
>>> lst
[1, 2, 3, 5]

>>> lst = [1, 2, 3]
>>> def foo2():
...     LST += [5] # ... 但是这个炸弹!
...
>>> foo2()
回溯(最近一次调用):
  File "", line 1, in 
  File "", line 2, in foo
UnboundLocalError:赋值前引用的局部变量“lst”

Huh? 为什么 foo2 炸弹而 foo1 跑了好?

答案与前面的示例问题相同,但不可否认的是更加微妙. foo1 不是在制造 赋值 to lst,而 foo2 is. 记住, LST += [5] 真的只是简写吗 LST = LST + [5],我们看到我们正试图 assign 值为 lst (因此Python假定它在局部作用域内). 然而,我们想要赋值的值 lst 是基于 lst 本身(同样,现在假定是在局部范围内),尚未定义. Boom.

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

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

>>> odd = lambda x : bool(x % 2)
>>> numbers = [n for n in range(10)]
>>> for i in range(len(numbers)):
...     如果奇怪(数字[我]):
...         del numbers[i] # BAD:从列表中删除迭代项
...
回溯(最近一次调用):
  	  File "", line 2, in 
IndexError:列表索引超出范围

在迭代列表或数组时从列表或数组中删除项是任何有经验的软件开发人员都熟悉的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():
...     返回[lambda x: I * x for I in range(5)]
>>> for multiplier in create_multipliers():
...     打印乘数(2)
...

您可能期望得到以下输出:

0
2
4
6
8

但实际上你得到:

8
8
8
8
8

惊喜!

这是由于Python的 后期绑定 该行为表示在调用内部函数时查找闭包中使用的变量的值. 所以在上面的代码中,无论何时调用任何返回的函数 i 是被查到的 在它被调用时的周围作用域中 (到那时,循环已经完成,所以 i 已经赋给了它的最终值4).

解决这个常见Python问题的方法有点小技巧:

>>> def create_multipliers():
...     返回[lambda x, i=i: i * x for i in range(5)]
...
>>> for multiplier in create_multipliers():
...     打印乘数(2)
...
0
2
4
6
8

Voilà! 我们在这里利用默认参数来生成匿名函数,以实现期望的行为. 有些人会称之为优雅. 有些人会称之为微妙. 有些人讨厌它. 但是如果您是一名Python开发人员,无论如何都要理解这一点.

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

假设你有两个文件, a.py and b.py,每一项进口另一项,详情如下:

In a.py:

进口b

def f ():
    返回b.x
	
printf ()

And in b.py:

导入一个

x = 1

def g ():
    打印一个.f()

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

>>> 导入一个
1

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

答案是单纯的 存在 在Python中,循环导入本身并不是问题. 如果一个模块已经被导入,Python就不会尝试重新导入它. 然而, 取决于每个模块试图访问另一个模块中定义的函数或变量的点, 你可能真的会遇到问题.

回到我们导入的例子 a.py在美国,进口没有问题 b.py,因为 b.py 不需要任何东西吗 a.py 待定义 在导入时. 唯一的参考文献 b.py to a 是对 a.f(). 但是那个电话打进来了 g() 什么也没有 a.py or b.py 调用 g(). 所以生活是美好的.

但是如果我们试图进口会发生什么呢 b.py (无需事先导入) a.py,即):

>>> 进口b
回溯(最近一次调用):
  	  File "", line 1, in 
  	  文件“b.py", line 1, in 
    导入一个
  	  文件”.py", line 6, in 
	printf ()
  	  文件”.Py”,第4行,f
	返回b.x
'module'对象没有属性'x'

Uh-oh. 这可不太好! 这里的问题是,在进口的过程中 b.py,它试图进口 a.py,这反过来又调用 f(),它试图访问 b.x. But b.x 还没有定义. 因此, AttributeError 异常.

至少有一个解决方案是相当微不足道的. 简单的修改 b.py 进口 a.py within g():

x = 1

def g ():
    导入一个	#只有当g()被调用时才会被计算
    打印一个.f()

不,当我们导入它时,一切正常:

>>> 进口b
>>> b.g()
1	#自模块'a'调用'printf ()'后第一次打印
1	#打印第二次,这是我们对'g'的调用

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

Python的优点之一是丰富的库模块,它是“开箱即用”的。. 但结果是, 如果你没有有意识地避开它, 遇到一个模块的名称与Python附带的标准库中具有相同名称的模块之间的名称冲突并不那么困难(例如, 您可能有一个名为 email.py 在您的代码中,这将与同名的标准库模块发生冲突)。.

这可能会导致一些粗糙的问题, 例如导入另一个库,该库反过来尝试导入模块的Python标准库版本,但是, 因为你有一个同名的模块, 另一个包错误地导入了您的版本,而不是Python标准库中的版本. 这就是严重的Python错误发生的地方.

护理应, 因此, 要避免使用与Python标准库模块中相同的名称. 对您来说,更改包中模块的名称要比修改文件名称简单得多 Python增强建议(PEP) 向上游请求更改名称并设法获得批准.

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

考虑下面的文件 foo.py:

导入系统

def酒吧(我):
    如果I == 1:
        提高KeyError (1)
    如果I == 2:
        提高ValueError (2)

def坏():
    e =无
    try:
        酒吧(int (sys.argv [1]))
    除KeyError外:
        print(关键错误)
    除非ValueError为e:
        打印(“价值错误')
    打印(e)

bad()

在Python 2上,这运行得很好:

$ python foo.py 1
关键错误
1
$ python foo.py 2
值错误
2

但是现在让我们在Python 3上尝试一下:

$ python3 foo.py 1
关键错误
回溯(最近一次调用):
  文件“foo.py", line 19, in 
    bad()
  文件“foo.Py”,第17行
    打印(e)
UnboundLocalError:赋值前引用的局部变量“e”

这里刚刚发生了什么? “问题”在于,在Python 3中,异常对象在 除了 block. 这样做的原因是, 否则, 它将在内存中保持堆栈帧的引用周期,直到垃圾收集器运行并从内存中清除引用. 更多的技术细节是可用的 here).

避免此问题的一种方法是维护对异常对象的引用 的范围 除了 块,使其保持可访问性. 下面是使用此技术的前一个示例的版本, 从而产生对Python 2和Python 3都友好的代码:

导入系统

def酒吧(我):
    如果I == 1:
        提高KeyError (1)
    如果I == 2:
        提高ValueError (2)

def好():
    异常 =无
    try:
        酒吧(int (sys.argv [1]))
    除KeyError外:
        异常= e
        print(关键错误)
    除非ValueError为e:
        异常= e
        打印(“价值错误')
    打印(异常)

good()

在Py3k上运行这个:

$ python3 foo.py 1
关键错误
1
$ python3 foo.py 2
值错误
2

Yippee!

(顺便说一下,我们的 Python招聘指南 讨论了将代码从Python 2迁移到Python 3时需要注意的其他一些重要差异.)

常见错误10:误用 __del__ 方法

假设你在一个叫做 mod.py:

import foo

类酒吧(对象):
   	    ...
    def __del__(自我):
        foo.清理(自我.myhandle)

然后你试着从 another_mod.py:

进口模
Mybar = mod.Bar()

你会得到一个丑陋的 AttributeError 异常.

Why? 因为,据报道 here,当解释器关闭时,模块的全局变量都被设置为 None. 因此,在上面的例子中,在点上 __del__ 调用时,名称是 foo 已经设置为 None.

这个更高级的Python编程问题的解决方案是使用 atexit.注册() 而不是. 这种方式, 当你的程序完成执行时(正常退出时), 这是), 已注册的处理程序被踢开 before 翻译被关闭了.

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

import foo
进口atexit

def清理(处理):
    foo.清理(处理)


类酒吧(对象):
    def __init__(自我):
        ...
        atexit.注册(清理、自我.myhandle)

此实现提供了一种干净可靠的方式,可以在正常程序终止时调用任何所需的清理功能. 显然,这取决于 foo.清理 来决定如何处理绑定到该名称的对象 self.myhandle,但你懂的.

Python陷阱:一旦你了解细微差别就可以避免

Python是一种强大而灵活的语言,具有许多可以大大提高生产力的机制和范式. 与任何软件工具或语言一样, though, 对其能力的理解或欣赏有限,有时可能是一种障碍,而不是好处, 让一个人处于众所周知的“知道足够危险”的状态.

熟悉Python的关键细微差别, 例如(但绝不限于)本文中提出的较为高级的编程问题, 将有助于优化语言的使用,同时避免Python中一些最常见的错误.

你可能也想看看我们的 Python面试内幕指南 获取有关面试问题的建议,以帮助识别Python专家.

我们希望本文中的提示对您有所帮助,并欢迎您的反馈.

Tags

就这一主题咨询作者或专家.
预约电话

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

Toptal开发者

加入总冠军® 社区.