学习 Python 一年,这次终于弄懂了浅拷贝和深拷贝

简介: 话说,网上已经有很多关于 Python 浅拷贝和深拷贝的文章了,不过好多文章看起来还是决定似懂非懂,所以决定用自己的理解来写出这样一篇文章。

话说,网上已经有很多关于 Python 浅拷贝和深拷贝的文章了,不过好多文章看起来还是决定似懂非懂,所以决定用自己的理解来写出这样一篇文章。


当别人一提起 Python 中的复制操作,你会不会立马站起来说:“我会”,于是就有了如下操作:

import copy
 x = copy.copy(y)        # 浅拷贝我会了
 x = copy.deepcopy(y)    # 深拷贝我来了


那浅拷贝和深拷贝有什么区别呢,你能给我讲讲吗?


从引用 vs.拷贝说起

首先,我们要弄清楚什么是对象引用与对象拷贝(复制)。

对象引用

Python 中对象的赋值其实就是对象的引用。当创建一个对象,把它赋值给另一个变量的时候,Python 并没有拷贝这个对象,只是拷贝了这个对象的引用而已。

>>> a = 1
>>> b = a
>>> id(a) == id(b)
True
>>> x = [1, 2, 3]
>>> y = [x, 4]
>>> x
[1, 2, 3]
>>> y
[[1, 2, 3], 4]
>>> 
>>>> id(x) == id(y)
False
>>> id(x) == id(y[0])
True


如果这个过程不理解,可以看看下图:


image.png


当我们对 x 列表进行操作时,会发现 y 中也发生了意料之外的事情:

>>> x[1] = 2020
>>> y
[[1, 2020, 3], 4]


由于列表是可变的,修改 x 这个列表对象的时候,也会改变对象 y 中对 x 的引用。


所以当我们在原处修改可变对象时 可能会影响程序中其他地方对相同对象的其他引用,这一点很重要。如果你不想这样做,就需要明确地告诉 Python 复制该对象。

对象拷贝

如果你需要拷贝,可以进行如下操作:


  • 没有限制条件的分片表达式(L[:]
  • 工厂函数(如 list/dir/set)
  • 字典 copy 方法(X.copy())
  • copy 标准库模块(import copy)


举个例子,假设有一个列表 L 和一个字典 D:

>>> L = [2019, 2020, 2021]
>>> D = {'1':2019, '2':2020, '3':2021}
>>> 
>>> A = L[:]  # 区分 A=L 或 A = List(L)
>>> B = D.copy()  # 区分 B=D 
>>> A
[2019, 2020, 2021]
>>> B
{'1': 2019, '2': 2020, '3': 2021}


image.png


这样定义之后,当你修改 A 和 B 时,会发现并不会对原来的 L 跟 D 产生影响,因为,这就是对象的拷贝。

>>> A[1] = 'happy'
>>> B[3] = 'today'
>>> L, D
([2019, 2020, 2021], {'1': 2019, '2': 2020, '3': 2021})
>>> A, B
([2019, 'happy', 2021], {'1': 2019, '2': 2020, '3': 2021, 3: 'today'})

上述对列表和字典的拷贝操作默认都为浅拷贝:


  • 制作字典的浅层复制可以使用 dict.copy() 方法
  • 而制作列表的浅层复制可以通过赋值整个列表的切片完成,例如,copied_list = original_list[:]


说到这里,疑问就产生了?什么是浅拷贝?浅拷贝的对应深拷贝又该作何解释?

谈谈浅拷贝和深拷贝

官方文档定义:


浅层复制和深层复制之间的区别仅与复合对象 (即包含其他对象的对象,如列表或类的实例) 相关:

  • 一个 浅层复制 会构造一个新的复合对象,然后(在可能的范围内)将原对象中找到的 引用 插入其中。
  • 一个 深层复制 会构造一个新的复合对象,然后递归地将原始对象中所找到的对象的 副本 插入。

浅拷贝

浅拷贝:拷贝了最外围的对象本身,内部的元素都只是拷贝了一个引用而已。也就是,把对象复制一遍,但是该对象中引用的其他对象我不复制。


用通俗的话理解就是:你的橱柜(对象)里装着一🧺(篮子)🥚(鸡蛋),然后浅拷贝一下的意思。我只拷贝了最外面的这个橱柜,至于里面的内部元素(🧺和🥚)我并不拷贝。


当我们遇到简单的对象时,用上面的解释好像很好理解;如果遇到复合对象,就比如下列代码:

l1 = [3, [66, 55, 44], (3, 7, 21)]
l2 = list(l1)
l1.append(100)
print('l1:', l1)
print('l2:', l2)
l1[1].remove(55)
l2[1] += [33, 22]
l2[2] += (9, 9, 81)
print('l1:', l1)
print('l2:', l2)


代码解释:


  • l2l1的浅拷贝
  • 把 100 追加到l1,对l2没有影响
  • l1内部列表l1[1中的 55 删除,对l2也产生影响,因为l1[1]l2[1]绑定的是同一个列表
  • 对可变对象来说,l2[1引用的列表进行+=就地修改列表。这次修改导致l1[1]也发生了改变
  • 对元组来说,+= 运算符创建一个新元组,然后重新绑定给变量 l2[2]。这等同于 l2[2] = l2[2] + (10, 11)。现在,l1l2 中最 后位置上的元组不是同一个对象


把这段代码可视化出来如下:


image.png


深拷贝

深拷贝:外围和内部元素都进行了拷贝对象本身,而不是引用。也就是,把对象复制一遍,并且该对象中引用的其他对象我也复制。


对比上面的篮子和鸡蛋:你的橱柜(对象)里装着一🧺(篮子)🥚(鸡蛋),然后深拷贝一下的意思。把最外面的这个橱柜和里面的内部元素(🧺和🥚)全部拷贝过来。


image.png

from copy import deepcopy
l1 = [3, [66, 55, 44], (3, 7, 21)]
l2 = deepcopy(l1)
l1.append(100)
print('l1:', l1)
print('l2:', l2)
l1[1].remove(55)
l2[1] += [33, 22]
l2[2] += (9, 9, 81)
print('l1:', l1)
print('l2:', l2)


输出结果:


image.png

拷贝的特点

  1. 不可变类型的对象(如数字、字符串、和其他'原子'类型的对象)对于深浅拷贝毫无影响,最终的地址值和值都是相等的。也就是,"obj is copy.copy(obj)" 、"obj is copy.deepcopy(obj)"
  2. 可变类型的对象=浅拷贝: 值相等,地址相等 copy 浅拷贝:值相等,地址不相等 deepcopy 深拷贝:值相等,地址不相等
  3. 循环引用的对象如果对象有循环引用,那么这个朴素的算法会进入无限循环。deepcopy 函数会记住已经复制的对象,因此能优雅地处理循环引用。


循环引用:b 引用 a,然后追加到 a 中;deepcopy 会想办法复制 a,而 copy 会进入无限循环。如下面代码:

from copy import deepcopy, copy
a = [80, 90]
b = [a, 100]
a.append(b)
print("a:", a)
print("b:", b)
c = deepcopy(a)
print("c:", c)
d = copy(b)
print("d:", d)


输出结果:

a: [80, 90, [[...], 100]]
b: [[80, 90, [...]], 100]
c: [80, 90, [[...], 100]]
d: [[80, 90, [[...], 100]], 100]

深浅拷贝的作用

1,减少内存的使用 2,以后在做数据的清洗、修改或者入库的时候,对原数据进行复制一份,以防数据修改之后,找不到原数据。3. 可以定制复制行为,通过实现__copy()__deep__()方法来控制。

总结

拷贝其实在开始学好几个操作语句中,我们就已经使用过却可能不知道的(前 3 个),而且浅拷贝是 Python 的默认拷贝方式。拷贝的方法如下:


  1. 可变类型的切片操作:[:]
  2. 工厂函数(如 list/dir/set)
  3. 字典 copy 方法(X.copy())
  4. 然后就是 Python 有专门的 copy 标准库模块:包含两个方法copy()deepcopy()


浅拷贝就像是我只拷贝最外围的对象,对象中引用的其他对象我不复制。深拷贝就是完整的把对象和对象里的内容都拷贝过来。拷贝的目的:


  1. 为了节省内存
  2. 防止数据丢失。


后记:深浅拷贝的坑及难以理解的点也只在复合对象上,简单对象就是我们平常理解的复制。而针对非容器类型(如数字、字符串、和其他'原子'类型的对象)没有被拷贝一说。


相关文章
|
2月前
|
存储 Java 数据处理
(numpy)Python做数据处理必备框架!(一):认识numpy;从概念层面开始学习ndarray数组:形状、数组转置、数值范围、矩阵...
Numpy是什么? numpy是Python中科学计算的基础包。 它是一个Python库,提供多维数组对象、各种派生对象(例如掩码数组和矩阵)以及用于对数组进行快速操作的各种方法,包括数学、逻辑、形状操作、排序、选择、I/0 、离散傅里叶变换、基本线性代数、基本统计运算、随机模拟等等。 Numpy能做什么? numpy的部分功能如下: ndarray,一个具有矢量算术运算和复杂广播能力的快速且节省空间的多维数组 用于对整组数据进行快速运算的标准数学函数(无需编写循环)。 用于读写磁盘数据的工具以及用于操作内存映射文件的工具。 线性代数、随机数生成以及傅里叶变换功能。 用于集成由C、C++
317 0
|
2月前
|
存储 JavaScript Java
(Python基础)新时代语言!一起学习Python吧!(四):dict字典和set类型;切片类型、列表生成式;map和reduce迭代器;filter过滤函数、sorted排序函数;lambda函数
dict字典 Python内置了字典:dict的支持,dict全称dictionary,在其他语言中也称为map,使用键-值(key-value)存储,具有极快的查找速度。 我们可以通过声明JS对象一样的方式声明dict
184 1
|
2月前
|
算法 Java Docker
(Python基础)新时代语言!一起学习Python吧!(三):IF条件判断和match匹配;Python中的循环:for...in、while循环;循环操作关键字;Python函数使用方法
IF 条件判断 使用if语句,对条件进行判断 true则执行代码块缩进语句 false则不执行代码块缩进语句,如果有else 或 elif 则进入相应的规则中执行
274 1
|
2月前
|
存储 Java 索引
(Python基础)新时代语言!一起学习Python吧!(二):字符编码由来;Python字符串、字符串格式化;list集合和tuple元组区别
字符编码 我们要清楚,计算机最开始的表达都是由二进制而来 我们要想通过二进制来表示我们熟知的字符看看以下的变化 例如: 1 的二进制编码为 0000 0001 我们通过A这个字符,让其在计算机内部存储(现如今,A 字符在地址通常表示为65) 现在拿A举例: 在计算机内部 A字符,它本身表示为 65这个数,在计算机底层会转为二进制码 也意味着A字符在底层表示为 1000001 通过这样的字符表示进行转换,逐步发展为拥有127个字符的编码存储到计算机中,这个编码表也被称为ASCII编码。 但随时代变迁,ASCII编码逐渐暴露短板,全球有上百种语言,光是ASCII编码并不能够满足需求
166 4
|
3月前
|
JavaScript Java 大数据
基于python的网络课程在线学习交流系统
本研究聚焦网络课程在线学习交流系统,从社会、技术、教育三方面探讨其发展背景与意义。系统借助Java、Spring Boot、MySQL、Vue等技术实现,融合云计算、大数据与人工智能,推动教育公平与教学模式创新,具有重要理论价值与实践意义。
|
5月前
|
监控 数据安全/隐私保护 Python
微信自动抢红包免费版,2025微信抢红包神器,微信红包挂苹果版【python仅供学习】
这个模拟项目包含5个模块:核心监控逻辑、用户界面、配置管理、实用工具和主程序入口
|
Python
Python 中浅拷贝(copy)和深拷贝(deepcopy)
Python 中浅拷贝(copy)和深拷贝(deepcopy)
165 0
|
Java 程序员 Python
python学习13-面向对象的三大特征、特殊方法和特殊属性、类的浅拷贝和深拷贝
python学习13-面向对象的三大特征、特殊方法和特殊属性、类的浅拷贝和深拷贝
|
Shell Python
Python(三十二)python类的浅拷贝与深拷贝
Python中的对象复制,深拷贝与浅拷贝
91 1

推荐镜像

更多