从赋值说起

之前在 python 变量相关的文章中,提到过赋值行为在 python 中和其他语言有何异同。说白了,其实就是默认传递引用,而不会拷贝整个对象。这种做法一个比较好的地方就是避免的在使用大型对象的时候,由于一些使用上的不规范而造成巨大的开销。但是凡事都有两面性,方便的同时,带来的坏处就是,同一个对象的引用可以有多个,那么只要这个对象的数据发生了变化,受影响的将是指向他的所有的引用。这种行为通常是我们不想看到的。

其实,这种行为,和 c++中的浅拷贝的行为是一直的。c++中也有引用的概念。浅拷贝通常也只是拷贝引用or 指针,而不会真正的拷贝整个对象。依稀记得,如果要在 C++中的自定义类型实现深拷贝,还需要自己实现拷贝构造函数。在 python 中,对于 list,dict 等数据结构,有一些现成的方法可以调用:

1
2
3
4
5
6
7
8
	a = [1, 2, 3]
	b = a //shallow copy
	c = a[:] //deep copy, get a new object
	g = list(a) //deep copy. cool!
	
	d = {1: 2, 3: 4}
	e = d //shallow copy
	f = d.copy()

需要注意的是:列表和字典上面所提到的深拷贝方法,都无法拷贝嵌套结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
In [12]: a
Out[12]: [1, [2, 3, 4], 5]

In [13]: b
Out[13]: [1, [2, 3, 4], 5]

In [14]: a[1][0] = 10000000

In [15]: a
Out[15]: [1, [10000000, 3, 4], 5]

In [16]: b
Out[16]: [1, [10000000, 3, 4], 5]

其中, a 中的列表,及时通过[:]形式进行拷贝,仍然无法对其中嵌套的列表进行深拷贝操作。这个时候,可以使用 copy 标准库内的 deepcopy 方法,来达到嵌套深拷贝的目的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
In [17]: import copy

In [18]: c = copy.deepcopy(a)

In [19]: c
Out[19]: [1, [10000000, 3, 4], 5]

In [20]: a
Out[20]: [1, [10000000, 3, 4], 5]

In [21]: c[1] = "fdsfsdsf"

In [22]: a
Out[22]: [1, [10000000, 3, 4], 5]

In [23]: c
Out[23]: [1, 'fdsfsdsf', 5]

从比较说起

python 中比较两个对象的是否相等,通常有两种形式:

  1. is
  2. ==

第一种形式,就是按照深拷贝的思想,测试两个引用所指的对象是否是相同,这里的相同是引用指向的内存地址相同,也就是判断两者是否是同一个对象 第二种形式,仅仅比较两个引用指向对象的值是否相等。并且,无论是第一种还是第二种,python 中的比较都会递归的进行,不会仅仅只比较顶层的元素。

重复赋值

这里说重复赋值不是指对某一个变量赋值多次,而指的是对某一个变量赋值多个重复的元素。当重复的这个元素是嵌套或者非嵌套的时候,都会有着截然不同的结果。

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

In [50]: b = a * 4

In [51]: b
Out[51]: [1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]

In [52]: c = [a] * 4

In [53]: c
Out[53]: [[1, 2, 3], [1, 2, 3], [1, 2, 3], [1, 2, 3]]

In [54]: a[1] = 44444

In [55]: a
Out[55]: [1, 44444, 3]

In [56]: b
Out[56]: [1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]

In [57]: c
Out[57]: [[1, 44444, 3], [1, 44444, 3], [1, 44444, 3], [1, 44444, 3]]

利用 id 分别查看一下 a,b,c 三个变量的地址就可以很容易的解释因重复嵌套元素所造成的这种问题

1
2
3
4
5
6
7
8
In [60]: id(a)
Out[60]: 4329904392

In [61]: id(b)
Out[61]: 4328297536

In [62]: id(c[0])
Out[62]: 4329904392

b 在赋值的时候,对 a 做了4次的重复,我们都知道,序列的重复,分片,合并都会生成一个新的对象而不会改动原来的引用。所以 b 中的12个元素都是仅属于 b 的。由于对 c 赋值的时候,语义表现的是重复4个元素为 a 的嵌套列表给 c,重复的操作看起来只对顶层元素生效了,因为顶层元素是 a,是一个列表,也可以称作是一个引用,所以 c 这个列表中的四个元素,都是 a 列表单位引用。此时,a 和 c 存在共享引用的现象,自然在更改 a 的时候。就改动了 c。所以说,一个嵌套的形式,无形中在重复元素赋值的时候,加深了深度,与序列的重复只对顶层元素生效一起,导致了上面的现象。