变量的赋值

在 Python 中,要创建一个列表 [1, 2, 3] 并赋值给变量 a 的语法是这样的:a = [1, 2, 3]。通常我们称 a变量名[1, 2, 3]变量的值

给一个变量赋值的操作实际上就是将一个变量名指向一个对象,a = [1, 2, 3] 就相当于将变量名 a 指向 [1, 2, 3] 这个列表对象。此时将变量 a 再赋值给变量 bb = a,相当于将变量 b 也指向 [1, 2, 3] 列表。最终 ab 指向的是同一个 [1, 2, 3] 列表。

1
2
3
4
5
6
7
8
9
>>> a = [1, 2, 3]
>>> b = a
>>> a is b
True
>>> a.append(4)
>>> a
[1, 2, 3, 4]
>>> b
[1, 2, 3, 4]

在 Python 中可以通过 is 来比较两个变量是否为同一个对象,所以 ab 本质上是同一个对象,只不过是这个对象的两个 名字 罢了。

此时,将列表 [4, 5, 6] 赋值给变量 aa = [4, 5, 6],再次对变量 a 进行修改,会得到什么样的结果呢?

1
2
3
4
5
6
>>> a = [4, 5, 6]
>>> a.append(7)
>>> a
[4, 5, 6, 7]
>>> b
[1, 2, 3, 4]

可以看到,将变量 a 重新赋值以后,再次更改 a 的值,b 的值并没有跟着变化。实际上,在执行 a = [4, 5, 6] 的时候,a 的指针变成了指向新的列表 [4, 5, 6],而 b 的指针并没有改变,依旧指向原来的列表 [1, 2, 3, 4]

我们可以用示例图来看下这个过程。

  1. Python 解释器在执行 a = [1, 2, 3] 时,变量 a 通过一个指针指向列表 [1, 2, 3]
1
1
  1. 在执行 b = a 时,变量 b 也通过一个指针指向列表 [1, 2, 3]
2
2
  1. 在执行 a.append(4) 时,实际上就是在更改变量 a 和变量 b 所指向的同一个列表对象。
3
3
  1. 将变量 a 重新赋值,变量 b 依旧指向原来的列表对象。
4
4
  1. 此时再次对变量 a 进行更改 a.append(7),变量 b 的值不变。
5
5

对象的拷贝

由上面的赋值操作我们可以知道,在 Python 中如果将一个变量赋值给另一个变量,那么它们最终将指向同一个对象。然而有些时候我们需要创建一个与原对象有着相同值的新对象,但是不想让它们还是同一个对象,即如果改变其中一个对象,那么另一个对象不会跟着改变。这个时候,就需要用到对象的拷贝的知识了。

对于拷贝,Python 专门提供了一个 copy 模块。拷贝可以分为 浅拷贝深拷贝

  1. 先来看看浅拷贝
1
2
3
4
5
6
7
8
9
>>> import copy
>>> a = [1, 2, 3]
>>> b = copy.copy(a)
>>> b
[1, 2, 3]
>>> a == b
True
>>> a is b
False

copy.copy 就是 Python 提供的浅拷贝方法。浅拷贝会在内存中重新创建一个新的对象,但对象中内部的元素还是对原对象内部元素的引用。

来看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> import copy
>>> a = [[1, 2, 3], [4, 5, 6]]
>>> b = copy.copy(a)
>>> b
[[1, 2, 3], [4, 5, 6]]
>>> id(a)
14800096
>>> id(b)
14799136
>>> a.append(11)
>>> a[0].append(22)
>>> a
[[1, 2, 3, 22], [4, 5, 6], 11]
>>> b
[[1, 2, 3, 22], [4, 5, 6]]

上面的例子中,首先创建了一个列表 a,其内部还嵌套了另外两个列表,然后对 a 进行了 浅拷贝 操作,得到 b

执行 a.append(11) 操作的时候,不会对 b 产生影响,这是 浅拷贝 的性质决定的,b 是一个新的对象。

执行 a[0].append(22) 的时候,b 的值会跟着改变,这就是上面所说的,浅拷贝时,新对象中内部的元素还是对原对象内部元素的引用。

由此可见,浅拷贝 只是对嵌套对象的顶层拷贝,并不会拷贝其内部元素。因此有些时候操作还是会产生一些副作用,要想完全复制一个对象,就需要用到 深拷贝

  1. 深拷贝

Python 提供了 copy.deepcopy 可以对对象进行深度拷贝。深拷贝 同样会创建一个新的对象,并且还会将原对象内部的元素以递归的方式进行完全拷贝。这样就实现了完全复制一个对象的目的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> import copy
>>> a = [[1, 2, 3], [4, 5, 6]]
>>> b = copy.deepcopy(a)
>>> b
[[1, 2, 3], [4, 5, 6]]
>>> id(a)
14799976
>>> id(b)
14800096
>>> a.append(11)
>>> a[0].append(22)
>>> a
[[1, 2, 3, 22], [4, 5, 6], 11]
>>> b
[[1, 2, 3], [4, 5, 6]]

使用 深拷贝,当改变变量 a 的值的时候,就不会对变量 b 的值产生影响了。这就是所谓的 深拷贝

上面已经对 深拷贝浅拷贝 进行了大致的介绍,不过还有些细节部分需要我们注意。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> import copy
>>> a = (1, 2, 4)
>>> b = copy.copy(a)
>>> c = copy.deepcopy(a)
>>> b
(1, 2, 4)
>>> c
(1, 2, 4)
>>> id(a)
52975576
>>> id(b)
52975576
>>> id(c)
52975576
>>> a is b is c
True

出现以上现象其实是因为元组为不可变对象,因此无需重新分配内存并创建一个新的对象。所以,对于不可变对象来说,浅拷贝深拷贝 最终的结果相同,都是指向原有的对象。但也有例外,就是当元组内部嵌套可变类型的时候。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> import copy
>>> a = ([1, 2], 3)
>>> b = copy.copy(a)
>>> c = copy.deepcopy(a)
>>> a[0].append(33)
>>> a
([1, 2, 33], 3)
>>> b
([1, 2, 33], 3)
>>> c
([1, 2], 3)
>>> id(a)
53628080
>>> id(b)
53628080
>>> id(c)
53628560

上面示例的结果其实并没有脱离 浅拷贝深拷贝 的特性,只不过是由于顶层对象是不可变类型,没有复制的必要,所以 浅拷贝 的对象和原对象还是同一个对象,原对象改变,浅拷贝 的对象也跟着改变。但是由于元组内部嵌套的对象是列表,列表又是可变类型,所以 深拷贝 就会递归的对其进行拷贝。

其实拷贝对于不可变对象来说,作用不大,真正有用的地方是在于可变对象的拷贝操作,并且只有被拷贝对象为嵌套对象的时候,才能够体现出差异。浅拷贝 只是对对象的顶层拷贝,深拷贝 是对对象的完全拷贝。

对于序列的 切片 操作,数据类型的构造器,实际上它们都相当于 浅拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> a = [1, 2, 3]
>>> b = a[:]
>>> b
[1, 2, 3]
>>> a is b
False

>>> a = [1, 2, 3]
>>> b = list(a)
>>> b
[1, 2, 3]
>>> a is b
False

对于自定义对象,还可以通过实现 __copy____deepcopy__ 两个方法来定义 浅拷贝深拷贝 的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import copy

class C(object):
def __str__(self):
return 'c'

def __copy__(self):
return 'copy c'

def __deepcopy__(self, memodict={}):
return 'deepcopy c'

c = C()
print(c)
c1 = copy.copy(c)
print(c1)
c2 = copy.deepcopy(c)
print(c2)
1
2
3
c
copy c
deepcopy c

你并不需要死记硬背来记下拷贝的特性,实际上,你只需要了解 Python 中可变类型和不可变类型的特点,就能够很容易理解了。在实际工作中,你可以根据自己的需要,使用 浅拷贝 或者 深拷贝 来解决遇到的问题。