Python是一门非常容易上手的语言,对于初学者非常友好。但是,这种简单易上手的代价是隐藏在它背后的巨大复杂度。这也就造成了在很多时候,会发生一些看起来很神奇的事情。其实,这只是对Python背后的机制不了解造成的。今天,笔者带大家梳理一下Python中容易踩到的那些”坑“。

变量是“盒子”吗?

很多人都认为,Python里的变量像是一个“盒子”。它里面可以装各种各样的数据,当我们把其他值赋给这个变量的时候,就相当于把盒子里的数据倒掉,然后再装新的值。

但实际上,Python里的数据才是占主导地位的,变量像是一个”便利贴”。把当我们把其他值赋给这个变量的时候,相当于把变量这个便利贴从原数据上撕掉,再贴到新的数据盒子上。

pyt2.jpg

嗯?不信?我们可以做个实验。

1
2
3
4
5
6
7
8
9
10
11
a = [1, 2, 3]
b = a
b[1] = 4
print(a)
print(id(a))
print(id(b))

>>>
[1, 4, 3]
1490148643008
1490148643008

当然,对于不可变对象,你会发现更改b对于a没有影响。这是因为在你更改b的那一瞬间,由于a和b指向的是不可变对象,Python会创建出一个新的对象,然后把b这个便利贴贴过去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
a = (1, 2, 3)
b = a
print(id(a))
print(id(b))
b = (1, 2, 4)
print(a)
print(id(a))
print(id(b))

>>>
3123113077952
3123113077952
(1, 2, 3)
3123113077952
3123113846144

真的新建了对象吗?

假设我们有一个场景,需要创建一个二维数组九宫格,那初学者可能会直接这样写:

1
2
a = [0, 0, 0]
b = [a] * 3

但是当你给这个b赋值的时候,你会发现,事情好像不太对…

1
2
3
4
5
6
7
8
n = 1
for i in range(3):
for j in range(3):
b[i][j] = n
n += 1

>>>
[[7, 8, 9], [7, 8, 9], [7, 8, 9]]

问题就出在b = [a] * 3这一步。在这一步里,这三个a实际上是等价的,它们指向的是同一个数据。我们如果想实现真正的内部各不相同的二维数组,需要这样:

1
2
3
4
5
6
7
8
9
10
b = [[0] * 3 for _ in range(3)]
n = 1
for i in range(3):
for j in range(3):
b[i][j] = n
n += 1
print(b)

>>>
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]

[0] * 3这一步,由于整数0是不可变对象,所以生成的[0, 0, 0]更改其中一个并不影响其他值(注意,这三个0指向同一个数据。实际上,Python中所有相同的小整数之间全部等价)。在下一步中,我们使用列表推导式的方法强迫Python生成三个不同的[0, 0, 0]列表对象出来。所以可以得到正确的结果。

深拷贝 / 浅拷贝

让我们来看这样一个场景:

1
2
3
4
5
6
7
8
9
from copy import copy

a = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
b = copy(a)
b[0][0] = 10
print(a)

>>>
[[10, 2, 3], [4, 5, 6], [7, 8, 9]]

哎,怎么回事。明明进行拷贝了,怎么b的更改依然会影响到a?赶紧打印一下他们的id看看:

1
2
3
4
5
6
print(id(a))
print(id(b))

>>>
1720442550080
1720442540544

明明id确实不一样,说明他们的确不是指向同一个数据,那到底是为什么呢?实际上,a和b确实不同,但a与b这两个列表中的元素指向的却是同一个数据,如下图:

pyt3.png

我们需要使用深拷贝deepcopy,才能把列表里的列表(或其他结构,或更深的嵌套维度)全部进行真正的拷贝。

1
from copy import deepcopy

pyt4.png

未完待续…