Python 2.x陷阱和地雷

Python 2.x gotchas and landmines

我的问题的目的是用Python加强我的知识库,并更好地了解它,包括了解它的错误和意外。为了使事情具体化,我只对cpython解释器感兴趣。

我在找一些类似于从我的PHP地雷中学到的东西问题是,有些答案对我来说是众所周知的,但有两个是令人毛骨悚然的。

更新:很明显,有一个或者两个人因为我问了一个在堆栈溢出之外已经得到部分回答的问题而心烦意乱。作为某种妥协,这里是URLhttp://www.ferg.org/projects/python_gotchas.html

请注意,这里的一个或两个答案是来自上面提到的网站上所写内容的原始答案。


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

示例:考虑将参数默认为当前时间:

1
2
3
4
5
6
7
8
9
>>>import time
>>> def report(when=time.time()):
...     print when
...
>>> report()
1210294387.19
>>> time.sleep(5)
>>> report()
1210294387.19

when参数不变。定义函数时会对其进行评估。在重新启动应用程序之前,它不会更改。

策略:如果你默认None的参数,然后在看到它时做一些有用的事情,你就不会跳过这一点:

1
2
3
4
5
6
7
8
9
10
>>> def report(when=None):
...     if when is None:
...         when = time.time()
...     print when
...
>>> report()
1210294762.29
>>> time.sleep(5)
>>> report()
1210294772.23

练习:确保你理解:为什么会这样?

1
2
3
4
5
6
7
8
9
10
11
12
>>> def spam(eggs=[]):
...     eggs.append("spam")
...     return eggs
...
>>> spam()
['spam']
>>> spam()
['spam', 'spam']
>>> spam()
['spam', 'spam', 'spam']
>>> spam()
['spam', 'spam', 'spam', 'spam']


您应该知道类变量是如何在Python中处理的。考虑以下类层次结构:

1
2
3
4
5
6
7
8
class AAA(object):
    x = 1

class BBB(AAA):
    pass

class CCC(AAA):
    pass

现在,检查以下代码的输出:

1
2
3
4
5
6
7
8
>>> 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

惊讶?如果您记住类变量是作为类对象的字典在内部处理的,您就不会这样做。对于读取操作,如果在当前类的字典中找不到变量名,则会搜索父类。因此,下面的代码再次出现,但有解释:

1
2
3
4
5
6
7
8
9
10
11
# 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

在类实例中处理类变量也是如此(将此示例视为上述示例的延续):

1
2
3
4
5
6
7
8
>>> 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

循环和lambda(或任何闭包,真的):变量由名称绑定

1
2
3
4
5
6
7
funcs = []
for x in range(5):
  funcs.append(lambda: x)

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

解决方法是创建单独的函数或按名称传递参数:

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


动态绑定使变量名中的拼写错误异常难以找到。花半个小时来修复一个小错误是很容易的。

编辑:一个例子…

1
2
3
4
5
for item in some_list:
    ... # lots of code
... # more code
for tiem in some_other_list:
    process(item) # oops!


我对python最大的惊喜之一是:

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

除了在更新元组的第一个条目后引发typeerror之外,这是可以预期的工作方式!因此,执行+=语句后,a将成为([42, 43, 44],),但无论如何都会有例外。如果你另一方面试试这个

1
2
3
a = ([42],)
b = a[0]
b += [43, 44]

你不会出错的。


1
2
3
4
try:
    int("z")
except IndexError, ValueError:
    pass

这不起作用的原因是,indexError是您正在捕获的异常类型,valueError是要为其分配异常的变量的名称。

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

1
2
3
4
try:
    int("z")
except (IndexError, ValueError):
    pass

前一段时间有很多关于隐藏语言特性的讨论:Python的隐藏特性。其中提到了一些陷阱(还有一些好东西)。

你也可能想看看Python疣。

但对我来说,整数除法是一个很好的方法:

1
2
>>> 5/2
2

你可能想要:

1
2
>>> 5*1.0/2
2.5

如果你真的想要这种(类似C的)行为,你应该写:

1
2
>>> 5//2
2

因为这也适用于浮动(当您最终进入python 3时,它也会工作):

1
2
>>> 5*1.0//2
2.0

gvr解释了整数除法是如何在python的历史上起作用的。


名单分割给我带来了很多痛苦。我实际上认为下面的行为是一个错误。

定义列表X

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

访问索引2:

1
2
>>> x[2]
30

正如你所料。

将列表从索引2切片到列表结尾:

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

正如你所料。

访问索引7:

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

如你所料。

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

1
2
>>> x[7:]
[]

????

补救方法是在使用列表切片时进行大量测试。我希望我能得到一个错误。更容易调试。


你的包裹里不包括一个__init__py。那个人有时还是会让我抓狂。


JamesDumay雄辩地让我想起了另一个PythonGotcha:

并不是所有的python的"附带电池"都很棒。

詹姆斯的具体例子是http库:httpliburlliburllib2urlparsemimetoolsftplib。有些功能是重复的,有些功能是完全不存在的,例如重定向处理。坦白说,这很可怕。

如果这些天我必须通过HTTP获取一些东西,我将使用来自yum项目的urlgrabber模块。


唯一让我吃惊的是,我和塞顿的吉尔打交道。如果出于任何原因,您希望cpython中的python线程并发运行…它们不是,这在Python人群甚至是guido自己身上都有很好的记录。

对cpython线程和一些在幕后进行的事情的详细而漫长的解释,以及为什么与cpython的真正并发是不可能的。http://jessenoller.com/2009/02/01/python-threads-and-the-global-explorer-lock/


默认情况下,浮动不以完全精度打印(不带repr):

1
2
3
4
5
x = 1.0 / 3
y = 0.333333333333
print x  #: 0.333333333333
print y  #: 0.333333333333
print x == y  #: False

repr打印的数字太多:

1
2
3
print repr(x)  #: 0.33333333333333331
print repr(y)  #: 0.33333333333300003
print x == 0.3333333333333333  #: True


无意中将旧样式和新样式类混合在一起可能会导致看似神秘的错误。

假设您有一个由超类A和子类B组成的简单类层次结构。当B被实例化时,必须首先调用A的构造函数。下面的代码正确执行此操作:

1
2
3
4
5
6
7
8
9
10
class A(object):
    def __init__(self):
        self.a = 1

class B(A):
    def __init__(self):
        super(B, self).__init__()
        self.b = 1

b = B()

但是,如果您忘记创建一个NewStyle类并像这样定义它:

1
2
3
class A:
    def __init__(self):
        self.a = 1

你会得到这个回溯:

1
2
3
4
5
6
Traceback (most recent call last):
  File"AB.py", line 11, in <module>
    b = B()
  File"AB.py", line 7, in __init__
    super(B, self).__init__()
TypeError: super() argument 1 must be type, not classobj

与此问题相关的另外两个问题是489269和770134


1
2
3
4
5
def f():
    x += 1

x = 42
f()

结果生成一个UnboundLocalError,因为本地名称是静态检测的。另一个例子是

1
2
3
4
5
6
def f():
    print x
    x = 43

x = 42
f()


您不能使用locals()['x']=任何更改本地变量值的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
This works:

>>> x = 1
>>> x
1
>>> locals()['x'] = 2
>>> x
2

BUT:

>>> def test():
...     x = 1
...     print x
...     locals()['x'] = 2
...     print x  # *** prints 1, not 2 ***
...
>>> test()
1
1

这实际上在这里的一个答案中烧焦了我,所以,因为我在一个函数之外测试了它,得到了我想要的更改。之后,我发现它提到并与"dive in to python"中的globals()进行了对比。参见示例8.12。(尽管它没有注意到通过locals()进行的更改将在上面所示的顶层工作。)


使用嵌套列表重复列表

这让我今天陷入困境,浪费了一个小时的调试时间:

1
2
3
4
5
6
>>> x = [[]]*5
>>> x[0].append(0)

# Expect x equals [[0], [], [], [], []]
>>> x
[[0], [0], [0], [0], [0]]   # Oh dear

说明:python列表问题


x是一个列表时,x += [...]x = x + [...]不同。`

1
2
3
4
5
6
7
8
9
>>> x = y = [1,2,3]
>>> x = x + [4]
>>> x == y
False

>>> x = y = [1,2,3]
>>> x += [4]
>>> x == y
True

一个创建新列表,另一个在适当位置修改


在需要实例变量时使用类变量。大多数情况下,这不会造成问题,但如果它是一个可变的值,它会导致惊喜。

1
2
class Foo(object):
    x = {}

但是:

1
2
3
4
5
>>> f1 = Foo()
>>> f2 = Foo()
>>> f1.x['a'] = 'b'
>>> f2.x
{'a': 'b'}

您几乎总是需要实例变量,这些变量要求您在__init__中分配:

1
2
3
class Foo(object):
    def __init__(self):
        self.x = {}


通过比较,python 2有一些令人惊讶的行为:

1
2
3
4
5
6
>>> print x
0
>>> print y
1
>>> x < y
False

发生什么事?repr()援救:

1
2
>>> print"x: %r, y: %r" % (x, y)
x: '0', y: 1


如果为函数内的变量赋值,python将假定该变量是在该函数内定义的:

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

使用global x(或python 3中的nonlocal x)声明要设置在函数外部定义的变量。


range(end_val)的值不仅严格小于end_val,而且严格小于int(end_val)。对于floatrange的论点,这可能是一个意想不到的结果:

1
2
3
from future.builtins import range
list(range(2.89))
[0, 1]


由于"真实性",这是有道理的:

1
2
>>>bool(1)
True

但你可能不希望它走另一条路:

1
2
>>>float(True)
1.0

如果您要将字符串转换为数字,并且数据具有真/假值,那么这可能是一个很好的方法。


如果以这种方式创建列表:

1
2
3
arr = [[2]] * 5
print arr
[[2], [2], [2], [2], [2]]

然后创建一个数组,所有元素都指向同一个对象!这可能会造成真正的混乱。考虑一下:

1
arr[0][0] = 5

那么如果你打印arr

1
2
print arr
[[5], [5], [5], [5], [5]]

初始化数组的正确方法是使用列表理解:

1
2
3
4
5
6
7
arr = [[2] for _ in range(5)]

arr[0][0] = 5

print arr

[[5], [2], [2], [2], [2]]