python常见的坑

本文总阅读量

前记

本文主体思想和内容来自于Buggy Python Code: The 10 Most Common Mistakes That Python Developers Make。由于结合内容写出自己平时遇到一些坑,所以与原文有些不同之处,出入较大。

1. 滥用可变对象作为函数参数的默认值

Python允许您给函数的参数提供默认值,但是在使用可变对象作为默认参数时,它可能会导致一些混淆。

1
2
3
4
5
6
7
8
9
10
11
12
13
In [1]: def foo(bar=[]): 
...: bar.append("baz")
...: return bar
...:

In [2]: foo()
Out[2]: ['baz']

In [3]: foo()
Out[3]: ['baz', 'baz']

In [4]: foo()
Out[4]: ['baz', 'baz', 'baz']

写下这个代码时,是想让代码在参数时已经初始化为一个list,之后可以对该参数进行操作,最后返回该参数,但是Python每次调用时都会给现有的foo()的bar默认值添加”bar”.
造成这样的原因是,Python在执行的时候,会把关键字参数存在于函数的__defaults__里面,在foo函数中,虽然调用的foo行为不一样,但函数里的bar对象的一样的,也就是把bar=[]这个对象存放于__defaults__里,由于bar是一个可变对象,每次调用都会用到与上一次相同的bar对象,进行处理后返回同一个bar,可以通过调用以下方法查看foo函数的关键字参数。

1
2
3
In [5]: foo.__defaults__       

Out[5]: (['baz', 'baz', 'baz', 'baz', 'baz'],)

同时要注意的是,在使用Python的类方法时也会发生这样的情况

1
2
3
4
5
6
7
8
9
In [1]: class Foo(object): 
...: bar = []
...:
In [2]: foo = Foo()
In [3]: foo.bar.append(1)

In [4]: foo1 = Foo()
In [5]: foo1.bar
Out[5]: [1]

2.错误的使用类变量

先看看如下示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
In [1]: class A(object): 
...: x=1
...:

In [2]: class B(A):
...: pass
...:

In [3]: class C(A):
...: pass
...:

In [4]: print(A.x, B.x, C.x)
1 1 1

会发现一切都和预想的一样

1
2
3
4
In [5]: B.x = 2                                                                                                    

In [6]: print(A.x, B.x, C.x)
1 2 1

这个也完成OK没/有任何问题,接下来试一试改A.x

1
2
3
4
5
6
In [7]: A.x = 2                                                                                                                                                             
In [8]: print(A.x, B.x, C.x) 2 2 2

In [9]: A.x = 3
In [10]: print(A.x, B.x, C.x)
3 2 3

可以看到,C.x会根据A.x进行改变,而B不会进行改变,这是因为在Python中,类变量在内部作为字典处理,并遵循解析顺序(MRO)的方法。因此在上面的代码中,由于在C类中找不到该属性x,因此将在其基类中查找(仅A在上面的示例中,尽管Python支持多个继承)。因此,C.x实际上是指A.x。而B.x在In[5]中做了赋值的操作,所以B类中存在属性x.

所以在使用继承时,要让子类共享父类的属性,那子类的属性一定不能进行赋值操作

3.Python作用域与定义的问题

先看看代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
In [1]: a = 10                                                                                                     

In [2]: def foo():
...: print(a)
...: a += 1
...:

In [3]: foo()
---------------------------------------------------------------------------
UnboundLocalError Traceback (most recent call last)
<ipython-input-3-c19b6d9633cf> in <module>
----> 1 foo()

<ipython-input-2-174dbfc7b886> in foo()
1 def foo():
----> 2 print(a)
3 a += 1
4

UnboundLocalError: local variable 'a' referenced before assignment

造成这样的原因是在对def作用域的变量进行赋值(定义)时,Python会自动将变量a视为def作用域,但此时def作用域并没有a这个变量,因此才会抛出该错误。可以通过global和nonlocal分别声明def里的变量是在def作用域还是全局作用域来解决这个问题。
之所以在赋值后面括号加上定义是因为,Python的=不仅是赋值,还带有定义,而真正引起错误的是
a += 1, a += 1其实是a = a + 1 此时的 = 带有的定义功能,才会造成这样的错误,试试下面的例子,由于append并不是赋值的操作,会发现Python并不会报错

1
2
3
4
5
6
7
8
9
10
In [4]: aaa = []                                                                                                   

In [5]: def foo():
...: print(aaa)
...: aaa.append('1')
...: return aaa
In [7]: print(foo())
['1']
['1']

此外,在该示例代码中的In[2]中相对于原文调整了x += 1和print的位置。这样的抛错会让人感觉更加奇怪,按道理应该会先执行print(a)再抛错的,但是却看不到有执行print(a)的痕迹,这时在写下一个与原文一样的代码,函数名命名为foo1,查看他们的字节码有什么变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
In [4]: def foo1(): 
...: a += 1
...: print(a)
...:
In [5]: import dis
In [6]: dis.dis(foo)
2 0 LOAD_GLOBAL 0 (print)
2 LOAD_FAST 0 (a)
4 CALL_FUNCTION 1
6 POP_TOP

3 8 LOAD_FAST 0 (a)
10 LOAD_CONST 1 (1)
12 INPLACE_ADD
14 STORE_FAST 0 (a)
16 LOAD_CONST 0 (None)
18 RETURN_VALUE

In [7]: dis.dis(foo1)
2 0 LOAD_FAST 0 (a)
2 LOAD_CONST 1 (1)
4 INPLACE_ADD
6 STORE_FAST 0 (a)

3 8 LOAD_GLOBAL 0 (print)
10 LOAD_FAST 0 (a)
12 CALL_FUNCTION 1
14 POP_TOP
16 LOAD_CONST 0 (None)
18 RETURN_VALUE

可以看出按照原文(foo1)会直接执行查找变量a,而在执行foo()的print(a)时,python也会去查找a,由于下面带有a+=1,Python的字节码从LOAD_GLOBAL变为LOAD_FAST也就是从def查找局部变量,所以就会导致print(a)执行的时候就报错了。

4.在迭代时修改list

先看一下代码以及报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
In [1]: odd = lambda x: bool(x % 2)                                                                                
In [2]: number_list = [n for n in range(10)]
In [3]: for i in range(len(number_list)):
...: if odd(number_list[i]):
...: del number_list[i]
...:
---------------------------------------------------------------------------
IndexError Traceback (most recent call last)
<ipython-input-3-6c6e0f879da3> in <module>
1 for i in range(len(number_list)):
----> 2 if odd(number_list[i]):
3 del number_list[i]
4

IndexError: list index out of range

这是一个常见的问题,但是经常会陷入一个误区,直到报错了发现不能在迭代的过程中del list的某一项数值。解决该错误的思路就是根据原有对象重新deepcopy一个对象再进行处理,下面是一个优雅的编程示例

1
2
3
In [4]: number_list = [n for n in number_list if not odd(n)]                                                       
In [5]: number_list
Out[5]: [0, 2, 4, 6, 8]

5.闭包中的变量一直不变

考虑以下示例代码

1
2
3
4
5
6
7
8
9
10
11
12
In [1]: def create_multipliers(): 
...: return [lambda x:i*x for i in range(5)]
...:

In [2]: for multiplier in create_multipliers():
...: print(multiplier(2))
...:
8
8
8
8
8

没想到他的输出竟然不是

1
2
3
4
5
0
2
4
6
8

这是因为PYthon的后期绑定行为,每当调用时,Python会在作用域查找对应的值(此时循环已经完成了,因此i的值是它的最终值:4)。
要想解决这个问题需要用到一些稍微奇妙的方法,就像问题1说到给参数配置一个动态变量时那样神奇。

1
2
3
4
5
6
7
8
9
10
11
12
13
In [3]: def create_multipliers(): 
...: return [lambda x, i=i:i*x for i in range(5)]
...:
...:
...:
In [4]: for multiplier in create_multipliers():
...: print(multiplier(2))
...:
0
2
4
6
8

6.异常处理中finally的return

在异常处理中,不管是否抛错,都会执行finally而且比return优先执行,这就会造成一个bug,如下示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
In [1]: def dig_dig2(index): 
...: try:
...: print("I'm in try")
...: if index < 0:
...: raise IndexError
...: else:
...: return index
...: except IndexError:
...: print("I'm in except")
...: return "except"
...: finally:
...: print("I'm in finally")
...: return "finally"
...:
...:
...: print(dig_dig2(12))
I'm in try
I'm in finally
finally

由于代码中在finally块语句中存在return语句,整个函数已结束,所以try块语句中return语句将永远得不到执行。

查看评论