【Python探寻之旅】可变对象,不可变对象,深拷贝,浅拷贝
- 1. 可变对象和不可变对象
- 1.1 “对象的值可以改变”是什么意思?
- 1.2 “对象的值不能改变”是什么意思?
- 1.3 总结
- 2. 用
=
赋值有什么问题 - 3. copy模块登场
- 4. 重新认识列表对象
- 5. 浅拷贝,深拷贝
- 5.1 浅拷贝(copy.copy())
- 浅拷贝的原理
- 浅拷贝举例
- 5.2 浅拷贝后,修改
z
中不可变对象元素的值 - 5.3 深拷贝(copy.deepcopy())
- 5.1 浅拷贝(copy.copy())
- 6.总结:用
=
赋值,浅拷贝,深拷贝的区别
看这篇文章之前,你需要了解:
什么是引用
在 Python 中,所有的变量名都是引用。
例如,对于整数变量来说,变量名是指向整数对象的引用,而不是整数对象本身的名字;
对于列表变量来说,变量名是指向列表对象的引用,而不是列表对象本身的名字。
1. 可变对象和不可变对象
想要知道什么是深拷贝和浅拷贝,必须要先知道什么是可变对象和不可变对象。、
在 Python 中,数据类型可以分为两大类:可变对象和不可变对象。
可变对象是指:对象的值可以改变;不可变对象是指:对象的值不能改变。
常见的可变对象有:
-
列表(list)
-
字典(dict)
-
集合(set)
常见的不可变对象有:
-
整数(int)
-
浮点数(float)
-
字符串(str)
-
元组(tuple)
-
布尔值(bool)
看到这里,你一定很懵。别急,我准备了一些例子,一定可以帮你搞定它们。
1.1 “对象的值可以改变”是什么意思?
例如:
我们创建一个列表a
,a
的地址记作id_old
。接下来,我们在a
的末尾添加一个元素,添加元素后,a
新的地址记作id_new
。
>>> a = [0,1]
>>> id_old = id(a) # 修改前a的地址
>>> a.append(1)
>>> id_new = id(a) # 修改后a的地址
>>> print(id_old == id_new)
True
比较id_old
和id_new
,发现id_old
与id_new
相等。这表示,添加元素前后,a
的地址没有发生变化。也就是说,Python直接在原地址上修改了a
的值。因此,列表a
的值是可以改变的。
这就是“对象的值可以改变”的意思。
1.2 “对象的值不能改变”是什么意思?
例如:
我们创建一个整数b
,b
的地址记作id_old
。接下来,我们修改b
的值,修改后,b
新的地址记作id_new
。
>>> b = 1
>>> id_old = id(b) # 修改前b的地址
>>> b = 2
>>> id_new = id(b) # 修改后b的地址
>>> print(id_old == id_new)
False
比较id_old
和id_new
,发现id_old
与id_new
不等。这表示,修改前后,b
的地址改变了。也就是说,Python不能在原地址上直接修改b
的值。
为什么会这样?
-
执行b=1的时候:
Python首先在内存中开辟一块空间,用来存储整数1。然后Python创建一个引用
b
,指向这块空间。is_old
就是这片空间的地址。
-
执行b=2的时候:
由于整数类型的性质,Python不能直接修改整数的值。因此,Python必须要再开辟一块空间,用来存储整数2。然后,Python让引用
b
指向新创建的空间。is_new
就是新创建空间的地址因此,整数
b
的值是不能改变的。如果想要“修改”b
的值,实际上并不是修改原来的整数对象,而是创建一个新的整数对象并将b
指向它。这就是“对象的值不能改变”的意思。
1.3 总结
对于上面两个例子,我画了一张图,来更直观的说明在修改前后,内存中发生了什么。
-
列表:Python可以直接对列表进行修改,因此,修改前后,内存中只有一个列表。
-
整数:Python不能修改整数的值。如果要修改整数的值,必须创建一个新的整数对象。因此,修改后,内存中有两个整数对象:一个是初始的整数1,一个是后来创建的整数2。
2. 用=
赋值有什么问题
当我们使用=
来赋值时,Python 不会创建一个新的对象,而只会创建一个新的引用。
例如,
>>> c = [1,2]
>>> id_c = id(c)
>>> d = c
>>> id_d = id(d)
>>> print(id_c == id_d)
True
当我们执行d=c时,Python不会为变量d
创建一个新的列表,而是让变量c
和d
共享同一个列表,c
和d
只不过是对同一个列表的两个引用。
如下图,c
和d
指向同一个列表对象
因此,当我们改变变量c
的值时,会发现,d
的值也变了。
>>> c.append(3)
>>> print(d)
[1,2,3]
原因是,c
和d
所共同指向的列表对象的值发生了变化,如下图
因此,d
的值也会变。
需要注意的是,对不可变对象来说,执行如下的代码
>>> x = 1 >>> y = x >>> x = 2 >>> print(y) 1
你会发现,
y
的值并没有随着x
的改变而改变。这是因为,整数是不可变对象。在执行y=x后,
x
、y
和整数1的关系如下图,此时,x
,y
是同一个整数对象的两个引用
执行x=2后,由于整数是不可变对象,因此Python会创建一个新的值为2的整数对象,并将这个整数对象赋给
x
,如下图
因此,
y
还是指向原来的整数,并没有受到影响,y
的值不会改变。
3. copy模块登场
在上面,我们看到,使用=
,不能复制列表,只能创建同一个列表对象的另一个引用。一旦原来的列表变量的值发生变化,那么新创建的列表变量的值也会随之改变。
那么,如何复制一份新的列表,使新列表的值不会受到旧列表的影响呢?
为了解决这个问题,我们需要引入copy模块。
在copy模块中,只有两个函数,copy
和deepcopy
看下面的例子
>>> import copy
>>> x = [1,2]
>>> y = copy.copy(x)
>>> z = copy.deepcopy(x)
>>> x.append(3)
>>> print(y)
>>> print(z)
[1,2]
[1,2]
可以看到,在使用了copy
和deepcopy
之后,在对x进行修改时,y和z的值就不会跟着发生变化了。
那么,copy
和deepcopy
有什么区别呢?
为此,我们需要重新认识一下列表对象,以及引入浅拷贝和深拷贝的概念
4. 重新认识列表对象
考虑下面两个列表变量,其中x
是一维列表,y
是二维列表
>>> x = [1,2,3]
>>> y = [[1,2,3],[4,5,6],[7,8,9]]
其中x
所对应的一维列表,在内存中是这样的:
x
是列表对象的引用。在列表对象中,存有三个地址,分别是:整数1的地址,整数2的地址以及整数3的地址。
至于y
所对应的二维列表,在内存中是这样的:
y是二维列表对象的引用。在二维列表对象中,存着三个一维列表对象的地址。在一维列表对象中,分别存着整数对象的地址。
5. 浅拷贝,深拷贝
知道列表对象在内存中是什么样子的,就可以引出浅拷贝和深拷贝了。
5.1 浅拷贝(copy.copy())
浅拷贝的原理
假设我们令
>>> y = [[1,2,3],[4,5,6],[7,8,9]]
>>> z = copy.copy(y)
那么,在内存中,执行的操作如下:
首先,Python创建一个新的二维列表对象,并令变量z
指向新的二维列表对象。
但需要注意的是,执行copy.copy()后,Python不会复制一维列表对象。旧的二维列表对象中的地址,和新的二维列表对象中的地址是相同的。也就是说,新旧两个二维列表对象指向相同的一维列表对象。
如下图
为了方便,这里省略了一维列表对象中的地址和整数对象
对于二维列表来说,浅拷贝只会复制二维列表对象,而不会复制一维列表对象和整数对象。
同样的,如果是三维列表,那么浅拷贝只会复制三维列表对象,而不会复制二维列表对象,一维列表对象和整数对象。
这就是浅拷贝的含义,浅拷贝只复制“最上层”的列表。
浅拷贝举例
令y
是一个二维列表,z
是y
的浅拷贝,执行下面的代码
>>> y = [[1,2,3],[4,5,6],[7,8,9]]
>>> z = copy.copy(y)
>>> z.append([1,2,3])
>>> print(y)
>>> z[0] = [1,2]
>>> print(y)
[[1,2,3],[4,5,6],[7,8,9]]
[[1,2],[4,5,6],[7,8,9]]
可以看到,向z
中添加元素时,y
的值不会变化。而修改z
的第一个列表的值时,y
的值也会发生变化。
当向z
中添加元素时, 内存中的操作如下图
可以看到,此时,在z
指向的二维列表对象中,新添加了一个地址4,地址4指向的就是新添加的元素。
不过,虽然z
中多了一个地址4,但这是对z
指向的二维列表对象所做的修改,y
指向的二维列表对象不会改变。因此向z
中添加元素,不会影响到y
的值。
当修改z
的第一个列表时,内存中的操作如下
图中标红的部分表示被修改的列表对象。
修改z
的第一个列表时,对于y
,z
分别指向的二维列表对象,它们不会发生变化。改变的是一维列表对象的值。
可以看到,y
和z
中,都有指向红色列表的地址。因此一旦红色列表被修改,那么y
和z
的值都会改变。
5.2 浅拷贝后,修改z
中不可变对象元素的值
考虑下面的代码
>>> x = [1,2,3]
>>> z = copy.copy(x)
内存中的操作如下:
Python复制了一个新的一维列表对象,并令z
指向新的一维列表对象。
同时,旧一维列表中的地址,和新一维列表中的地址是相同的。也就是说,新旧两个一维列表指向相同的整数对象。
如下图
当我们修改z
中整数的值时:
>>> z[0] = 0
由于整数是不可变对象,而不可变对象不允许修改对象的内容,如果需要更改内容,必须创建一个新的对象。
因此我们执行z[0]=0时,实际上创建了一个新的整数对象,并令新的整数对象的值为0,如下图:
这时候,x[0]
的值没有改变,因此x
不会受到影响。
5.3 深拷贝(copy.deepcopy())
假设我们令
>>> y = [[1,2,3],[4,5,6],[7,8,9]]
>>> z = copy.deepcopy(y)
此时,Python会把y
中所有的元素,完完整整的复制一遍,并赋给z
。这时候,无论对z
进行什么操作,都不会影响y
的值了。
6.总结:用=
赋值,浅拷贝,深拷贝的区别
我们前面讲过了三种赋值方式,分别是:用=
直接赋值,浅拷贝(copy.copy()),以及深拷贝(copy.deepcopy())
现在,我们总结一下这三者的区别:
-
用
=
直接赋值:执行
y=x
时,若:- y和x都是不可变对象,那么y和x互不影响
- y和x都是可变对象,那么修改y的值,x的值也会随之变化;同理,修改x,y也会随之改变
-
浅拷贝
执行
y = copy.copy(x)
时,若-
y和x都是不可变对象,那么y和x互不影响
-
y和x都是可变对象,那么
-
向y中添加元素,x不变;同理,向x中添加元素,y不变
-
修改y中可变对象元素的值,x随之改变;同理,修改x中的可变对象元素,y也会随之改变
-
修改y中不可变对象元素的值,x不变;同理,修改x中的可变对象元素,y不变
-
-
-
深拷贝
执行
y = copy.deepcopy(x)
时,无论y和x是什么类型,y和x的值永远互不影响。