一文带你彻底了解Python中深拷贝与浅拷贝问题!

一、引言

通过Python中变量赋值的本质——“引用”的概念Python中函数参数传递的本质——引用传递两篇文章,我们分别知道,在Python中:

  • 执行为变量赋值语句,并不是将数值直接在变量所代表的位置处拷贝一份;
  • 函数进行参数传递时,也不是直接将实参指向的位置处保存的数值直接拷贝至形参所指向的位置处。

实际上,Python中的变量赋值和函数参数传递均采取“引用”方式。那么,是否意味着Python中不存在拷贝的概念?

对于上述问题,答案是否定的。实际上,在Python中还有一个专门的copy模块,专门用于进行所谓深拷贝浅拷贝

进一步地,Python官方文档中在介绍copy模块时指出:

  • Assignment statements in Python do not copy objects, they create bindings between a target and an object.
    Python中的赋值语句并不会拷贝对象,而是创建目标(即变量)和对象(如数据等)的关联(即引用)。
  • For collections that are mutable or contain mutable items, a copy is sometimes needed so one can change one copy without changing the other.
    对于可变类型集合或者包含可变元素的集合,拷贝有时的确是需要的,从而使得可以在修改一份拷贝时不影响原始数据。
  • This module provides generic shallow and deep copy operations.
    Python中的copy模块即提供通用的浅拷贝和深拷贝操作。

本文就将带你彻底了解Python中的拷贝问题。

二、浅、深拷贝初探

在Python官方文档中,对于浅、深拷贝的说明指出:

  • The difference between shallow and deep copying is only relevant for compound objects (objects that contain other objects, like lists or class instances):
    浅拷贝和深拷贝的概念只有在涉及复合类型对象(包含其他对象的对象,如列表,类的实例)时才有意义。
  • A shallow copy constructs a new compound object and then (to the extent possible) inserts references into it to the objects found in the original.
    浅拷贝会构造一个新的复合对象,并且向复合对象中插入原复合对象中所包含对象的引用。
  • A deep copy constructs a new compound object and then, recursively, inserts copies into it of the objects found in the original.
    深拷贝会构造一个新的复合对象,并且会以递归的方式将原复合对象中的元素也拷贝一份,然后再将这些元素的引用插入新的复合对象中。

下面通过一段代码初步演示上述说明的含义:

import copy


a = [[11, 22], [33, 44]]
b = copy.copy(a)
c = copy.deepcopy(a)

print()
print("比较分别通过浅、深拷贝后得到的列表b、c是否和a的地址相同:")
print("id(a) = %x" % id(a))
print("id(b) = %x" % id(b))
print("id(c) = %x" % id(c))

print()
print("比较分别通过浅、深拷贝后得到的列表b、c,是否产生了新的空间保存a中的元素:")
print("id(a[0]) = %x" % id(a[0]))
print("id(b[0]) = %x" % id(b[0]))
print("id(c[0]) = %x" % id(c[0]))
print("id(a[1]) = %x" % id(a[1]))
print("id(b[1]) = %x" % id(b[1]))
print("id(c[1]) = %x" % id(c[1]))

a[0].append(55)
a.append(66)

print()
print("更改列表a之后,a、b、c三个列表的值为:")

print("a = ", a)
print("b = ", b)
print("c = ", c)

上述代码的运行结果为:

比较分别通过浅、深拷贝后得到的列表b、c是否和a的地址相同:
id(a) = 7fb01c8b2b88
id(b) = 7fb01c8b2788
id(c) = 7fb01c8b2f88


比较分别通过浅、深拷贝后得到的列表b、c,是否产生了新的空间保存a中的元素:
id(a[0]) = 7fb01c8b2508
id(b[0]) = 7fb01c8b2508
id(c[0]) = 7fb01c8b2f48
id(a[1]) = 7fb01c8b24c8
id(b[1]) = 7fb01c8b24c8
id(c[1]) = 7fb01c8b2fc8


更改列表a之后,a、b、c三个列表的值为:
a = [[11, 22, 55], [33, 44], 66]
b = [[11, 22, 55], [33, 44]]
c = [[11, 22], [33, 44]]

由运行结果可知:

  • 使用copy.copy()实现的是浅拷贝,拷贝出来的列表b产生了新的空间,但是并没有重新拷贝列表a中的元素,还是以引用的方式保存了列表a的元素;
  • 使用copy.deepcopy()实现的是深拷贝,拷贝出来的列表c也产生了新的空间,而且连列表a中的元素也被重新拷贝了一份,并通过引用的方式保存了新拷贝出来的元素。

因此,实际上执行上述第5、6行代码的结果如下图所示:

根据上图也不难理解为什么对列表a分别修改其第一个元素和追加一个元素后,列表b、c的输出结果如程序运行结果所示。

三、拷贝可变和不可变类型

在第一部分中,我们提到,在Python官方文档中有这样一句话:

对于可变类型集合或者包含可变元素的集合,拷贝有时的确是需要的,从而使得可以在修改一份拷贝时不影响原始数据。

实际上,此处强调拷贝所适用的场景是可变类型集合或者包含可变元素的集合这一点并非画蛇添足,因为Python的浅、深拷贝对于可变和不可变类型集合的效果不一致。

1. 拷贝元素为不可变类型的可变复合对象

针对可变类型集合对象(如:列表),如果其中的元素是不可变类型(如:元组),那么分别对该集合进行浅、深拷贝后的结果如何?请看下列代码:

import copy


a = [(11, 22), (33, 44)]
b = copy.copy(a)
c = copy.deepcopy(a)

print()
print("比较分别通过浅、深拷贝后得到的列表b、c是否和a的地址相同:")
print("id(a) = %x" % id(a))
print("id(b) = %x" % id(b))
print("id(c) = %x" % id(c))

print()
print("比较分别通过浅、深拷贝后得到的列表b、c,是否产生了新的空间保存a中的元素:")
print("id(a[0]) = %x" % id(a[0]))
print("id(b[0]) = %x" % id(b[0]))
print("id(c[0]) = %x" % id(c[0]))
print("id(a[1]) = %x" % id(a[1]))
print("id(b[1]) = %x" % id(b[1]))
print("id(c[1]) = %x" % id(c[1]))

a.append(66)

print()
print("更改列表a之后,a、b、c三个列表的值为:")

print("a = ", a)
print("b = ", b)
print("c = ", c)

上述代码运行结果为:

比较分别通过浅、深拷贝后得到的列表b、c是否和a的地址相同:
id(a) = 7f89ed084508
id(b) = 7f89ed0844c8
id(c) = 7f89ed084b88


比较分别通过浅、深拷贝后得到的列表b、c,是否产生了新的空间保存a中的元素:
id(a[0]) = 7f89ed0846c8
id(b[0]) = 7f89ed0846c8
id(c[0]) = 7f89ed0846c8
id(a[1]) = 7f89ed084708
id(b[1]) = 7f89ed084708
id(c[1]) = 7f89ed084708


更改列表a之后,a、b、c三个列表的值为:
a = [(11, 22), (33, 44), 66]
b = [(11, 22), (33, 44)]
c = [(11, 22), (33, 44)]

分析上述代码的运行结果,可知:

  • 列表a、b、c的地址均不同,表明此时a经过浅、深拷贝,均会产生新的内存空间;
  • 列表a、b、c的元素地址均相同,表明此时在新地址处的列表b、c,其中元素指向和列表a中元素指向一致,即经浅、深拷贝后,系统均没有为列表b、c再额外开辟空间存储元素。

即,上述执行上述第5、6行代码的结果如下图所示:

因此,针对保存了不可变类型元素的可变复合对象,如上述保存了元组的列表:

  • 经过浅、深拷贝后,均会产生新的列表空间,因为列表是可变类型,这样可以在修改新拷贝出的列表时不改变原列表;
  • 经过浅、深拷贝后,均不会产生新的空间保存列表中的元组类型元素,因为元组是不可变类型,因为没有必要产生新的空间保存元素。

2. 拷贝元素为可变类型的不可变复合对象

针对不可变类型集合对象(如:元组),如果其中的元素是可变类型(如:列表),那么分别对该集合进行浅、深拷贝后的结果又是如何?请看下列代码:

import copy


a = ([11, 22], [33, 44])
b = copy.copy(a)
c = copy.deepcopy(a)

print()
print("比较分别通过浅、深拷贝后得到的列表b、c是否和a的地址相同:")
print("id(a) = %x" % id(a))
print("id(b) = %x" % id(b))
print("id(c) = %x" % id(c))

print()
print("比较分别通过浅、深拷贝后得到的列表b、c,是否产生了新的空间保存a中的元素:")
print("id(a[0]) = %x" % id(a[0]))
print("id(b[0]) = %x" % id(b[0]))
print("id(c[0]) = %x" % id(c[0]))
print("id(a[1]) = %x" % id(a[1]))
print("id(b[1]) = %x" % id(b[1]))
print("id(c[1]) = %x" % id(c[1]))

a[0].append(55)

print()
print("更改列表a之后,a、b、c三个列表的值为:")

print("a = ", a)
print("b = ", b)
print("c = ", c)

上述代码运行结果为:

比较分别通过浅、深拷贝后得到的列表b、c是否和a的地址相同:
id(a) = 7f7765bf48c8
id(b) = 7f7765bf48c8
id(c) = 7f7765bf4948


比较分别通过浅、深拷贝后得到的列表b、c,是否产生了新的空间保存a中的元素:
id(a[0]) = 7f7765bf8508
id(b[0]) = 7f7765bf8508
id(c[0]) = 7f7765bf8748
id(a[1]) = 7f7765bf84c8
id(b[1]) = 7f7765bf84c8
id(c[1]) = 7f7765bf8fc8


更改列表a之后,a、b、c三个列表的值为:
a = ([11, 22, 55], [33, 44])
b = ([11, 22, 55], [33, 44])
c = ([11, 22], [33, 44])

分析上述代码可知,当待拷贝集合为不可变类型,且其中保存的元素为可变类型,则:

  • 进行浅拷贝时,此时仅相当于元组a又多了一个别名b,既没有产生新的元组空间,更没有为元组中的元素产生新的空间;
  • 进行深拷贝时,既产生了新的空间保存元组c,也产生了新的空间保存元组中的列表元素。

于是,执行上述第5、6行代码后的结果如下图所示:

3. 结论

  • 拷贝可变类型集合(如:列表、字典)时,浅、深拷贝均会产生新的空间存储该类型集合,对于其中的元素:
    • 当元素为可变类型时,只有深拷贝才会产生新的空间存储元素;
    • 当元素为不可变类型时,浅、深拷贝都不会产生新的空间来存储元素。
  • 拷贝不可变类型集合(如:元组)时,浅拷贝仅相当于为该集合又起了一个别名,深拷贝时,仅当其中的元素为可变类型时,才会产生新的空间来存储元素。

四、Python中的拷贝等价操作

1. 切片操作

在Python中,切片操作[]copy.copy()一样是浅拷贝,具体请见下列代码的运行结果:

a = [[11, 22], [33, 44]]
b = a[:]

print("id(a) = %x" % id(a))
print("id(b) = %x" % id(b))

print("id(a[0]) = %x" % id(a[0]))
print("id(b[0]) = %x" % id(b[0]))

上述代码的运行结果为:

id(a) = 7f7f48dfc3c8
id(b) = 7f7f48dfc388
id(a[0]) = 7f7f48df8d08
id(b[0]) = 7f7f48df8d08

2. 字典对象的copy方法

在Python中,字典对象的copy()方法实现的也是浅拷贝,具体请见下列代码:

dictionary = dict(name="法外狂徒张三", age=40, children_ages=[12, 14])
dictionary_copy = dictionary.copy()

print("id(dictionary) = %x" % id(dictionary))
print("id(dictionary_copy) = %x" % id(dictionary_copy))

dictionary['children_ages'].append(16)

print("dictionary = ", dictionary)
print("dictionary_copy = ", dictionary_copy)

上述代码的运行结果为:

id(dictionary) = 7f0a1debc1b0
id(dictionary_copy) = 7f0a1fc7b678
dictionary = {‘name’: ‘法外狂徒张三’, ‘age’: 40, ‘children_ages’: [12, 14, 16]}
dictionary_copy = {‘name’: ‘法外狂徒张三’, ‘age’: 40, ‘children_ages’: [12, 14, 16]}

猜你喜欢

转载自blog.csdn.net/weixin_37780776/article/details/105940740