应当了解的Python拷贝

BG74

Posted by Blue Geek on March 10, 2020

应当了解的Python拷贝

By 青衣极客 Blue Geek In 2020-03-10

大体上来看,拷贝可以分为两种:深拷贝和浅拷贝。所谓“深拷贝”,是指经过该操作得到的数据与原始数据内容一致,但存储空间不同;所谓“浅拷贝”是指该操作所得数据与原始数据存储在同一地址,只是换了一个别名。按照逻辑可知,深拷贝会比较耗时,因为需要对数据内容逐个复制,但是能够保证在操作备份数据的时候不影响原始数据;浅拷贝通常只是修改别名或者得到指针,不需要对原始数据逐个复制,因此速度快,但对备份的修改直接影响原始数据。

在任何一种编程语言中,都会支持对数据的拷贝,但是不同的编程语言对拷贝支持的程度不一样。比如,在C/C++中,一般的值传递都是深拷贝,而传递指针或者引用都是浅拷贝(针对指针本身的复制除外)。而在Python中,这个问题稍微有点复杂,本文就是为了说清楚Python中的拷贝到底是怎么回事,以及我们该如何使用拷贝操作。

1. 基础类型的拷贝

Python中的基础类型包括bool,int,float,str等类型。通常我们在使用赋值运算符处理这些类型的时候很少去考虑究竟发生了什么,下面的例子可以告诉我们一些信息。

a = 1
b = a
print('id(a)={}, id(b)={}'.format(id(a), id(b)))

Output:

id(a)=4309896976, id(b)=4309896976

从拷贝操作的输入和输出的id可以看出:Python中的int类型,乃至所有类型,直接使用赋值运算符进行的拷贝都是浅拷贝。既然是浅拷贝,我们是否需要担心对备份数据的写操作会污染原始数据呢?先说一下答案:不会。下面的例子可以支持这一说法。

b = 2
print('a={}, b={}'.format(a, b))

Output:

a=1, b=2

明明是浅拷贝,但是对备份数据的修改为什么不会影响原始数据呢?这是因为Python中使用赋值运算符的时候,都会修改被赋值变量的指向。上面的例子中,b=2这一行所进行的操作是将 “b” 变量修改为整数 “2”存储的地址别名,而不是将“b”原先指向的内容修改为新的。所以,虽然基本类型的复制是“浅拷贝”,但是我们可以当成“深拷贝”来使用。

这样的话,又产生了一个问题:str类型的赋值又是怎样的呢?str与int等其他基本类型不一样,它有子元素,那么对备份数据的这些子元素的修改会不会污染原始数据呢?下面的例子将说明这个问题。

c = '123'
c[1] = '4'
print(c)

Output:

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-5-e0ac949badbe> in <module>
      1 c = '123'
----> 2 c[1] = '4'
      3 print(c)

TypeError: 'str' object does not support item assignment

这个报错说明了Python不允许修改str的子元素,即所有的str对象都是常量。这一规则将str与int等其他基本类型在拷贝这个操作上保持了一致。所以还是那个结论:基本数据类型的赋值操作实质上是浅拷贝,但是可以当成深拷贝使用,即对备份数据的修改不会影响原始数据。

2. 复合类型浅拷贝

事实上,在Python中,赋值运算符不仅对基本类型是浅拷贝,对所有所有类型都是浅拷贝。这里我们以list类型为例来说明一下这个规则。

list_a = [1,2,3,4,5]
list_b = list_a
print(id(list_a))
print(id(list_b))

Output:

4353845920
4353845920

复制操作的输入和输出对象id相同,表明确实是浅拷贝。接下来,我们修改备份数据的子元素,看看会发生什么情况。

list_b[1] = 6
print('list_a={}'.format(list_a))
print('list_b={}'.format(list_b))

Output:

list_a=[1, 6, 3, 4, 5]
list_b=[1, 6, 3, 4, 5]

修改“list_b”变量的子元素,“list_a”变量对应的子元素也发生了变化,这是符合浅拷贝的规则的。

3. 复合类型深拷贝

有时我们需要对符合类型使用深拷贝,那该怎样操作呢?还是以list类型为例。

list_a = [1,2,3,4,5]
list_b = list(list_a)
print(id(list_a))
print(id(list_b))

Output:

4355914880
4356592656

使用构造函数来完成深拷贝当然是一个方法,但是并不是所有的复合类型都提供了深拷贝的构造函数。如果不能使用构造函数完成,那么又该如何实现呢?copy模块是一个不错的助手。

import copy
list_b = copy.copy(list_a)
print(id(a))
print(id(b))

Output:

4309896976
4309897008

我们可以使用copy.copy()函数完成一般复合对象的深拷贝。但是拷贝这个事情到此并没有结束,下面还有一些诡异的现象。

4. 多层复合类型的拷贝

所谓多层复合类型,就是指复合类型的子元素也是复合类型。在这种情况下,使用构造函数或者copy.copy()函数能够完成深拷贝吗?

list_a = [[1,2], [3,4], [5,6]]
list_b = list(list_a)
print(id(list_a))
print(id(list_b))

Output:

4315048128
4354396480

使用构造函数完成拷贝之后,原始数据和备份数据的id不相同,可以认为是深拷贝,但是别高兴得太早。

list_b[0][0] = 100
print(list_a)

Output:

[[100, 2], [3, 4], [5, 6]]

这个实验的结果有点诡异,说好了是深拷贝,怎么会修改备份数据之后,原始数据发生了同样的改变?到这一步,我们不得不查看一下两者的子元素是否指向同一地址。

print(id(list_a[0]))
print(id(list_b[0]))

Output:

4489536608
4489536608

到这里我们才明白,原来构造函数这种方式的拷贝并不会对子元素进行深拷贝,那么copy.copy()函数能否完成子元素的深拷贝呢?

import copy
list_b = copy.copy(list_a)
print(id(list_a[0]))
print(id(list_b[0])

Output:

4489536608
4489536608

这个实验结果表明,copy.copy()函数也无法完成子元素的深拷贝。难道Python就没有办法实现彻底的深拷贝吗?非也,copy模块中还有一个神奇的函数deepcopy()可以帮助我们。

import copy
list_b = copy.deepcopy(list_a)
print(id(list_a[0]))
print(id(list_b[0]))

Output:

4489536608
4495039056

以上实验表明,子元素存储数据的地址不同,已经完成了子元素的深拷贝。我们还是使用最原始的方法检验一下,修改备份数据的子对象试试。

list_b[0][0] = -100
print(list_a)

Output:

[[100, 2], [3, 4], [5, 6]]

可以看到,原始数据对应位置的数据没有被修改,copy.deepcopy()函数完成的是完全的深拷贝操作。

总结一下Python中的拷贝:

  1. 赋值运算符所进行的拷贝都是浅拷贝,但是对于基础类型可以当成深拷贝使用

  2. 对于单层的复合类型,可以使用其构造函数或者copy.copy()完成深拷贝操作

  3. 对于多层复合类型,只能使用copy.deepcopy()完成深拷贝操作

最后再多说一句,深拷贝虽然安全,但是对速度影响很大,所以一般情况下都不要使用深拷贝操作,除非需要对原始数据进行保护。了解了Python的拷贝机制,再也不担心写坏内存了。

实验代码演示

1. 基础类型的拷贝

a = 1
b = a
print('id(a)={}, id(b)={}'.format(id(a), id(b)))
id(a)=4309896976, id(b)=4309896976
b = 2
print('a={}, b={}'.format(a, b))
a=1, b=2
c = '123'
c[1] = '4'
print(c)
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-5-e0ac949badbe> in <module>
      1 c = '123'
----> 2 c[1] = '4'
      3 print(c)


TypeError: 'str' object does not support item assignment

2. 复合类型浅拷贝

list_a = [1,2,3,4,5]
list_b = list_a
print(id(list_a))
print(id(list_b))
4353845920
4353845920
list_b[1] = 6
print('list_a={}'.format(list_a))
print('list_b={}'.format(list_b))
print('id(list_a)={}'.format(id(list_a)))
print('id(list_b)={}'.format(id(list_b)))
list_a=[1, 6, 3, 4, 5]
list_b=[1, 6, 3, 4, 5]
id(list_a)=4353845920
id(list_b)=4353845920
list_b = [6,5,4,3,2,1]
print('list_a={}'.format(list_a))
print('list_b={}'.format(list_b))
print('id(list_a)={}'.format(id(list_a)))
print('id(list_b)={}'.format(id(list_b)))
list_a=[1, 6, 3, 4, 5]
list_b=[6, 5, 4, 3, 2, 1]
id(list_a)=4353845920
id(list_b)=4354747120
import copy
list_c = copy.copy(list_a)
list_c[1] = 9
print('list_a={}'.format(list_a))
print('list_c={}'.format(list_c))
print('id(list_a)={}'.format(id(list_a)))
print('id(list_c)={}'.format(id(list_c)))
list_a=[1, 6, 3, 4, 5]
list_c=[1, 9, 3, 4, 5]
id(list_a)=4353845920
id(list_c)=4353922464
list_c = [6,5,4,3,2,1]
print('list_a={}'.format(list_a))
print('list_c={}'.format(list_c))
print('id(list_a)={}'.format(id(list_a)))
print('id(list_c)={}'.format(id(list_c)))
list_a=[1, 6, 3, 4, 5]
list_c=[6, 5, 4, 3, 2, 1]
id(list_a)=4353845920
id(list_c)=4356210768

3. 复合类型深拷贝

list_a = [1,2,3,4,5]
list_b = list(list_a)
print(id(list_a))
print(id(list_b))
4355914880
4356592656

4. 多层复合类型的拷贝

list_a = [[1,2], [3,4], [5,6]]
list_b = list(list_a)
print(id(list_a))
print(id(list_b))
4489601584
4490545264
list_b[0][0] = 100
print(list_a)
[[100, 2], [3, 4], [5, 6]]
print(id(list_a[0]))
print(id(list_b[0]))
4489536608
4489536608
import copy
list_b = copy.copy(list_a)
print(id(list_a[0]))
print(id(list_b[0]))
4489536608
4489536608
import copy
list_b = copy.deepcopy(list_a)
print(id(list_a[0]))
print(id(list_b[0]))
4489536608
4495039056
list_b[0][0] = -100
print(list_a)
[[100, 2], [3, 4], [5, 6]]

【青衣极客】公众号



COMMENT

博客评论区功能由Github Issue提供,提交Issue时请以本文标题为话题

"BG74-应当了解的Python拷贝"