13

天啦噜!仅仅5张图,彻底搞懂Python中的深浅拷贝

 3 years ago
source link: https://segmentfault.com/a/1190000038163495
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Python中的深浅拷贝

在讲深浅拷贝之前,我们先重温一下 is== 的区别。

在判断对象是否相等比较的时候我们可以用 is==

  • is: 比较两个对象的引用是否相同,即 它们的id 是否一样
  • == : 比较两个对象的值是否相同。

id() ,是Python的一个内置函数,返回对象的唯一标识,用于获取对象的内存地址。

如下

yIbiuiM.png!mobile

首先,会为整数1分配一个内存空间。 变量a 和 b 都指向了这个内存空间(内存地址相等),所以他们的id相等。

a is bTrue

但是,真的所有整数数字都这样吗? 答案是:不是! 只有在 -25 ~ 256范围中的整数才不会重新分配内存空间。

如下所示:

因为257 超出了范围,所以id不相同,所以 a is b 返回的值为False。

BbyqQfm.png!mobile

>>> a = 257
>>> b = 257
>>> print(id(a))
20004752
>>> print(id(b))
20001312
>>> print(a is b)
False
>>> print(a == b)
True

这样做是考虑到性能,Python对-5 到 256 的整数维护了一个数组,相当于一个缓存, 当数值在这个范围内,直接就从数组中返回相对应的引用地址了。如果不在这个范围内,会重新开辟一个新的内存空间。

is 和 == 哪个效率高?

相比之下, is 比较的效率更高,因为它只需要判断两个对象的id是否相同即可。

== 则需要重载__eq__ 这个函数,遍历变量中的所有元素内容,逐次比较是否相同。因此效率较低

浅拷贝 深拷贝

给变量进行赋值,有两种方法 直接赋值,拷贝

直接赋值就 = 就可以了。而拷贝又分为浅拷贝和深拷贝

先说结论吧:

  • 浅拷贝: 拷贝的是对象的引用,如果原对象改变,相应的拷贝对象也会发生改变
  • 深拷贝: 拷贝对象中的每个元素,拷贝对象和原有对象不在有关系,两个是独立的对象

光看上面的概念,对新手来讲可能不太好理解。来看下面的例子吧

赋值

a = [1, 2, 3]
b = a
print(id(a)) # 52531048
print(id(b)) # 52531048

定义变量a,同时将a赋值给b。打印之后发现他们的 id 是相同的。说明指向了同一个内存地址。

m2mUJzU.png!mobile

然后修改a的值,再查看他们的id

a = [1, 2, 3]
b = a
print(id(a))  # 46169960
a[1] = 0
print(a, b)  # [1, 0, 3] [1, 0, 3]
print(id(a))  # 46169960
print(id(b))  # 46169960

这时候发现修改后的a和b以及最开始的a的内存地址是一样的。也就是说a和b还是指向了那一块内存,只不过内存里面的[1, 2, 3] 变成了[1, 0, 3]

因为每次重新执行的时候内存地址都是发生改变的,此时的id(a) 的值46169960与52531048是一样的

所以我们就可以判断出,b和a的引用是相同的,当a发生改变的时候,b也会发生改变。

vyeqIzZ.png!mobile

赋值就是:你a无论怎么变,你指向谁,我b就跟着你指向谁。

拷贝

提到拷贝就避免不了可变对象和不可变对象。

MbI3umA.png!mobile

  • 可变对象: 当有需要改变对象内部的值的时候,这个对象的id不发生变化。
  • 不可变对象: 当有需要改变对象内部的值的时候,这个对象的id会发生变化。
a = [1, 2, 3]
print(id(a)) # 56082504
a.append(4)
# 修改列表a之后 id没发生改变,可变对象
print(id(a)) # 56082504

a = 'hello'
print(id(a)) # 59817760
a = a + ' world'
print(id(a)) # 57880072
# 修改字符串a之后,id发生了变化。不可变对象
print(a) # hello world

浅拷贝

拷贝的是不可变对象, 一定程度上来讲等同于赋值操作。 但是对于多层嵌套结构,浅拷贝只拷贝父对象,不拷贝内部的子对象。

使用 copy 模块的 copy.copy 进行浅拷贝。

import copy
a = [1, 2, 3]
b = copy.copy(a)
print(id(a))  # 55755880
print(id(b))  # 55737992
a[1] = 0
print(a, b) # [1, 0, 3] [1, 2, 3]

通俗的讲,我将现在的a 复制一份重新分配了一个内存空间。后面你a怎么改变,那跟我b是没有任何关系的。

uAjEvie.png!mobile

对于列表的浅拷贝还可以通过list(), list[:] 来实现

但是!我前面提到了对于多层嵌套的结构,需要注意

看下面的例子

import copy
a = [1, 2, [3, 4]]
b = copy.copy(a)

print(id(a)) # 23967528
print(id(b)) # 21738984
# 改变a中的子列表
a[-1].append(5)
print(a) # [1, 2, [3, 4, 5]]
print(b) # [1, 2, [3, 4, 5]]  ?? 为什么不是[1, 2, [3, 4]]呢?

b是由a浅拷贝得到的。我修改了a中嵌套的列表,发现b也跟着修改了?

如果还是不太理解,可以参考下图。LIST就是一个嵌套的子对象,指向了另外一个内存空间。所以浅拷贝只是拷贝了元素 12 和子对象的引用!

qEbQVzQ.png!mobile

另外一种情况,如果嵌套的是一个元组呢?

import copy
a = [1, 2, (3, 4)]
b = copy.copy(a)

# 改变a中的元组
a[-1] += (5,)
print(a) # [1, 2, (3, 4, 5)]
print(b) # [1, 2, (3, 4)]

我们发现浅拷贝得来的b并没有发生改变。因为元组是不可变对象。改变了元组就会生成新的对象。b中的元组引用还是指向了旧的元组。

深拷贝

所谓深拷贝呢,就是重新分配一个内存空间(新对象),将原对象中的所有元素通过递归的方式进行拷贝到新对象中。

在Python中 通过 copy.deepcopy() 来实现深拷贝。

y2eaiu6.png!mobile

import copy
a = [1, 2, [3, 4]]
b = copy.deepcopy(a)

print(id(a)) # 66587176
print(id(b)) # 66587688
# 改变a中的可变对象
a[-1].append(5)
print(a) # [1, 2, [3, 4, 5]]
print(b) # [1, 2, [3, 4]]  深拷贝之后字列表不会受原来的影响

结语

1、深浅拷贝都会对源对象进行复制,占用不同的内存空间

2、如果源对象没有子目录,则浅拷贝只能拷贝父目录,改动子目录时会影响浅拷贝的对象

3、列表的切片本质就是浅拷贝

史上最全Python资料汇总(长期更新)。隔壁小孩都馋哭了 --- 点击领取

nYn6VnE.png!mobile


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK