图解Python赋值、浅拷贝和深拷贝

518 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第21天,点击查看活动详情

前言

关于Python赋值、深拷贝、浅拷贝,在面试和工作中都是热们话题。小白很容易在这个问题上傻傻分不清,今天我们就以图例来说说它们之间的区别。看完这篇,再也不会搞混它们的区别了。

基本概念

先来看看赋值、浅拷贝和深拷贝的定义:

  • 赋值:简单的对象引用,指向同一个引用地址;
  • 浅拷贝:只拷贝父对象,但不会拷贝内部的子对象;
  • 深拷贝:拷贝父对象和内部的子对象。

只看概念可能还不清晰,我们一一来说。

赋值

赋值的概念比较容易理解,了解引用就知道赋值是怎么回事了。我们以一个例子来看看赋值:

In [1]: a = [1,2,3]

In [2]: b = a

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

In [4]: id(a)
Out[4]: 140353018777152

In [5]: id(b)
Out[5]: 140353018777152

In [6]: a.append(4)

In [7]: a
Out[7]: [1, 2, 3, 4]

In [8]: b
Out[8]: [1, 2, 3, 4]

例子很简单,定义一个对象a,将a赋值给b。打印id相等,对a对象修改,b对象值也一样。

其实就是b引用了a,它们都指向同一个内存地址。看下图更好理解了,对b赋值操作时,只是将a指向的内存地址告诉b,这样一来它们都指向同一个内存地址,不管是a修改还是b修改,都是修改的同一个内存地址。

赋值操作的好处在于不会开辟新的内存空间,只复制了对象的引用地址,不存在其他的内存开销。

浅拷贝

浅拷贝不同于赋值,它会创建一个新的对象,并且会拷贝原对象的最外层对象,但不会拷贝内层对象,而是引用内层对象。

Python中实现浅拷贝有3种方式,分别是切片、copy函数及工厂函数。我们以copy来举例:

# 定义对象c,c包含了a和b内层对象
In [9]: a = [1,2,3]

In [10]: b = [4,5,6]

In [11]: c = [a, b]

In [12]: c
Out[12]: [[1, 2, 3], [4, 5, 6]]


## 浅拷贝
In [17]: import copy

In [18]: d = copy.copy(c)

# 浅拷贝的地址不一样
In [15]: id(c)
Out[15]: 140353019428544

In [19]: id(d)
Out[19]: 140353019424064

# 修改内层对象
In [20]: a.append(7)

In [21]: b.append(8)

In [22]: c
Out[22]: [[1, 2, 3, 7], [4, 5, 6, 8]]

# 值会变化
In [24]: d
Out[24]: [[1, 2, 3, 7], [4, 5, 6, 8]]

# 修改最外层对象
In [25]: c.append(9)

In [26]: c
Out[26]: [[1, 2, 3, 7], [4, 5, 6, 8], 9]

# 值不变
In [27]: d
Out[27]: [[1, 2, 3, 7], [4, 5, 6, 8]]

例子就是定义c原对象,这个对象中包含了a和b内层对象。当我们拷贝c对象时,其实是拷贝了c对象的最外层,而c对象的内层对象a、b则是做了引用的操作。修改内层对象,值会跟着变(引用);修改最外层对象,值不变(最外层拷贝了)。

我们用图来看更直观:

d浅拷贝c后,只是拷贝了最外层对象,但内存对象a、b还是引用。所以内层对象修改和最外层对象修改产生的结果不一样。

深拷贝

深拷贝相对于浅拷贝来说,就是深拷贝会拷贝对象的所有元素,包括原对象的内层对象。因此深拷贝的内存开销也是最大的。

python中是用copy.deepcopy实现深拷贝的。来看例子先:

In [28]: a = [1,2,3]

In [29]: b = [4,5,6]

In [30]: c = [a, b]

In [31]: d = copy.deepcopy(c)

In [32]: id(c)
Out[32]: 140353019340672

In [33]: id(d)
Out[33]: 140353019504448

In [34]: id(c[0]), id(c[-1])
Out[34]: (140353019553088, 140353018620864)

In [35]: id(d[0]), id(d[-1])
Out[35]: (140353019373696, 140353019083456)

In [36]: a.append(7)

In [37]: b.append(8)

In [38]: c
Out[38]: [[1, 2, 3, 7], [4, 5, 6, 8]]

In [39]: d
Out[39]: [[1, 2, 3], [4, 5, 6]]

In [40]: c.append(9)

In [41]: c
Out[41]: [[1, 2, 3, 7], [4, 5, 6, 8], 9]

In [42]: d
Out[42]: [[1, 2, 3], [4, 5, 6]]

例子和浅拷贝的相似,只是d对象是深拷贝了c对象。从例子可以看出,d和c指向地址不一样,并且内层对象的指向也不一样。这样也就说明了深拷贝是将原对象所有元素都拷贝了一份,后面无论对原对象的内层外层对象修改,d对象都不会受到影响。

用图来表示直观点:

三者的区别

  • 赋值:将对象引用,指向同一个内存地址;
  • 浅拷贝:只拷贝对象的最外层,而内层对象是引用;
  • 深拷贝:将对象全部拷贝,包括最外层和内层所有对象。

还有一些细节需要注意:

  • 对于非容器类型(如字符串、数字)没有拷贝的概念,都是直接引用。
  • 在深拷贝中,如果拷贝对象包含了不可变的数据类型(如元组),也只能得到浅拷贝。

小结

本文用图例总结了Python赋值、浅拷贝和深拷贝的细节和区别。搞清楚它们的原理,在工作中才不会用错拷贝了。