Python 2.x常见问题和陷阱

64
我的问题的目的是加强我对Python的知识体系,并更好地了解它,包括了解其缺陷和惊喜。为了具体化,我只对CPython解释器感兴趣。
我正在寻找类似于我从PHP地雷问题中学到的内容,其中一些答案对我来说很熟悉,但有几个答案令人毛骨悚然。
更新: 显然有一两个人因为我在Stack Overflow之外提出了一个部分回答的问题而感到不满。作为某种妥协,这是URL http://www.ferg.org/projects/python_gotchas.html 请注意,这里已经有一两个答案是原创的,来源于上述网站的内容。

不确定从2.5升级到2.6是否有太多的“陷阱”,如果您的意图是针对Python 2.X系列,最好将标题更改为2.X。 - monkut
http://www.ferg.org/projects/python_gotchas.html 中的列表有什么问题? - S.Lott
2
@hop 目前此问题的最佳答案在ferg.org页面中未提及。也许如果Guido编写了ferg.org页面并且我知道它的存在,那么我就不会再费心了,但是没有一个人能够全知道所有事情。 - David
NVM,那个答案似乎消失了? - David
1
@S.Lott -出了什么问题?ferg.org链接失效了。 - Peter M. - stands for Monica
显示剩余2条评论
23个回答

85

默认参数中的表达式在函数定义时计算,而不是在调用时计算。

例如:考虑将一个参数默认值设置为当前时间:

>>>import time
>>> def report(when=time.time()):
...     print when
...
>>> report()
1210294387.19
>>> time.sleep(5)
>>> report()
1210294387.19

when参数不会改变。它在定义函数时被计算,直到应用程序重新启动之前都不会改变。

策略: 如果你将默认参数设置为 None ,并且在使用时进行一些有用的操作,就不会遇到这个问题:

>>> def report(when=None):
...     if when is None:
...         when = time.time()
...     print when
...
>>> report()
1210294762.29
>>> time.sleep(5)
>>> report()
1210294772.23

练习:确保您已经理解:为什么会发生这种情况?

>>> def spam(eggs=[]):
...     eggs.append("spam")
...     return eggs
...
>>> spam()
['spam']
>>> spam()
['spam', 'spam']
>>> spam()
['spam', 'spam', 'spam']
>>> spam()
['spam', 'spam', 'spam', 'spam']

+1优秀观点!实际上,在类似的情境中我也曾依赖过这个,但我很容易看到会让不谨慎的人措手不及! - David
这是最为人所知的陷阱,但在了解它之前我从未被它咬过。 - hasen
3
Python的设计者做出了很多好的设计决策,但这并不是其中之一。 +1 - BlueRaja - Danny Pflughoeft
我放弃了/为什么会发生这种事? - Geoffrey
1
你可以使用元组作为默认参数,而不是列表。通常情况下, 默认值应该是不可更改的类型(NoneType、int、tuple等)。 - Abgan
显示剩余2条评论

62

你应该了解 Python 中类变量的处理方式。考虑以下类层次结构:

class AAA(object):
    x = 1

class BBB(AAA):
    pass

class CCC(AAA):
    pass
现在,请检查以下代码的输出结果:
>>> print AAA.x, BBB.x, CCC.x
1 1 1
>>> BBB.x = 2
>>> print AAA.x, BBB.x, CCC.x
1 2 1
>>> AAA.x = 3
>>> print AAA.x, BBB.x, CCC.x
3 2 3

惊讶吗?如果您记得类变量在内部被处理为类对象的字典,就不会感到惊讶了。对于读取操作,如果在当前类的字典中找不到变量名,则会在父类中搜索该变量名。因此,以下代码再次出现,但附有解释:

# AAA: {'x': 1}, BBB: {}, CCC: {}
>>> print AAA.x, BBB.x, CCC.x
1 1 1
>>> BBB.x = 2
# AAA: {'x': 1}, BBB: {'x': 2}, CCC: {}
>>> print AAA.x, BBB.x, CCC.x
1 2 1
>>> AAA.x = 3
# AAA: {'x': 3}, BBB: {'x': 2}, CCC: {}
>>> print AAA.x, BBB.x, CCC.x
3 2 3

处理类实例中的类变量也是同样的方式(将此示例视为上面示例的续集):

>>> a = AAA()
# a: {}, AAA: {'x': 3}
>>> print a.x, AAA.x
3 3
>>> a.x = 4
# a: {'x': 4}, AAA: {'x': 3}
>>> print a.x, AAA.x
4 3

41

循环和lambda(或任何闭包):变量通过名称绑定

funcs = []
for x in range(5):
  funcs.append(lambda: x)

[f() for f in funcs]
# output:
# 4 4 4 4 4

解决方法是创建一个单独的函数,或通过名称传递参数:

funcs = []
for x in range(5):
  funcs.append(lambda x=x: x)
[f() for f in funcs]
# output:
# 0 1 2 3 4

20

动态绑定使变量名的拼写错误难以发现。很容易花费半小时修复一个微不足道的错误。

编辑:一个例子...

for item in some_list:
    ... # lots of code
... # more code
for tiem in some_other_list:
    process(item) # oops!

1
+1 是的,这有时会让我困扰,不知道你能否在回答中提供一个例子? - David
3
我想是的,但这只是为了举例而已。实际发生的这种类型的错误往往会更加复杂。 - Algorias
13
您可以使用静态检查工具(如 PyLint)来查找这些错误--例如,tiem 将被标记为未使用的变量。 - Dzinx

18

Python 给我带来的最大惊喜之一就是:

a = ([42],)
a[0] += [43, 44]

这个代码看起来很正常,但在更新元组的第一个条目后却会引发TypeError! 所以在执行 += 语句后, a 将变为([42, 43, 44],),但仍会抛出异常。 另一方面,如果你尝试这样做

<code><code><code>a = ([42],)
b = a[0]
b += [43, 44]
</code></code></code>

你不会得到错误。


2
或者你可以简单地写成:a[0].extend([43, 44]) - Dzinx
2
哇,我认为在Python中更改并随后引发异常是一个错误。有什么理由可以解释这只是一个小问题吗? - Alfe
1
哇。我预料到会出错,但我没想到它实际上还会修改列表。这很丑陋。然而,我从来没有遇到过这种情况,因为我习惯于不使用元组来存储我想要更改的数据。即使它指向同一个列表,我也希望值保持不变。如果我想要具有可变位置元素,我会使用列表、字典或类。 - johannestaas
1
在 https://docs.python.org/2/faq/programming.html#why-does-a-tuple-i-item-raise-an-exception-when-the-addition-works 上提到了:当加法有效时,元组 i 项为什么会引发异常。 - selfboot

16
try:
    int("z")
except IndexError, ValueError:
    pass

这个代码不能正常工作的原因是您捕获的异常类型是IndexError,而将异常赋值给变量的名称是ValueError。

正确的捕获多个异常的代码是:

try:
    int("z")
except (IndexError, ValueError):
    pass

12

之前曾有讨论过Python中的隐藏语言特性:Python的隐藏特性。其中提到了一些陷阱(也有一些好东西)。

你也许想看看Python Warts

但对我来说,整数除法是一个坑:

>>> 5/2
2

您可能想要:

>>> 5*1.0/2
2.5

如果你真的想要这种类似于 C 的行为,你应该写成:
>>> 5//2
2

由于它也适用于浮点数(并且在您最终转到Python 3时也适用):

>>> 5*1.0//2
2.0

GvR解释了整数除法在Python的历史中是如何工作的。

4
确实是一个坑点。现在每次创建新的.py文件时,加上"from future import division"几乎成了本能反应。 - Chris Upchurch
1
假设5和2是变量,那么这就有意义了。否则你可以直接写5./2。 - Algorias
19
正确的解决方法很微妙:如果参数可能是复数,则将参数转换为float()是错误的;如果将0.0添加到参数中,则不会保留参数的符号,如果参数是负零。没有任何副作用的唯一解决方案是将一个参数(通常是第一个)乘以1.0。这样可以在float和complex类型下保持值和符号不变,并将int和long转换为具有相应值的float类型。 - Tom Dunham
在绝大多数不涉及复数的情况下,使用float()而不是*1.0更好。这是我的意见,因为它能够表达你真正的意图。通过乘以1.0来实现这一点有点混淆你想要的,任何不知道代码的读者可能会误以为1.0只是一个笔误(也许应该是10.0?)。 - Alfe
不,我认为1.0和1.之间没有区别,但是在使用它们时,如果你打算将任何东西升级到至少float而不降级复杂度,我建议适当地进行注释。当没有涉及复杂度时,我更喜欢使用float()。 - Alfe
显示剩余2条评论

11

如果你的包中不包括__init__.py文件,有时会让人感到困惑。


11

列表切片给我带来了很多烦恼。实际上,我认为以下行为是一个bug。

定义一个列表x:

>>> x = [10, 20, 30, 40, 50]

访问索引为2:

>>> x[2]
30

按照您的预期。

从索引2开始切片列表并切到列表末尾:

>>> x[2:]
[30, 40, 50]

如你所预期。

访问索引7:

>>> x[7]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: list index out of range

如你所预期的一样。

但是,尝试从索引7开始切片到列表结尾:

>>> x[7:]
[]

解决方法是在使用列表切片时添加很多测试。我希望只是得到一个错误提示而不是什么都不说。这样更容易调试。


我同意。它确实隐藏了那些偶发的错误。 - johannestaas
这个对于Python来说实际上是非常可预测的,当迭代空切片时非常有用。 - Asclepius
我认为这不是不良行为。如果您考虑切片背后的逻辑,这是可以预测和可取的...有点像使用列表推导来定义满足约束条件的列表索引。这很有用! - Patrick Da Silva

9
我遇到的唯一问题是CPython的GIL。如果您希望在CPython中使用Python线程并发运行... 那么它们不会并发运行,这在Python社区甚至Guido本人都有很好的文档记录。
关于CPython线程以及一些底层操作和原因的长而彻底的解释,以及为什么无法实现与CPython的真正并发。http://jessenoller.com/2009/02/01/python-threads-and-the-global-interpreter-lock/

2
请查看Python 2.6中提供的新的multiprocessing模块,它可以使用单独的进程进行类似线程的处理,如果全局解释器锁(GIL)困扰着您的话。 http://docs.python.org/library/multiprocessing.html - monkut
1
@David - 可能是 pyprocessing,它已经作为 multiprocessing 的一部分被纳入标准库。 - Ravi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接