官方文档:copy主题

源代码: Lib/copy.py

话说,网上已经有很多关于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

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

当我们对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}

这样定义之后,当你修改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 中最 后位置上的元组不是同一个对象

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

动手试一试,可以点此处

深拷贝

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

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

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)

输出结果:

拷贝的特点

  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__()方法来控制。

总结

看完这篇文章后,转身就跟你同桌说:

“x同学,听说你最近在学Python,你知道浅拷贝和深拷贝吗?”

“不知道,学得有点晕”

“没事,我来给你讲讲:”

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

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

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

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

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

要是你的同桌还是不懂,你就把这篇文章甩给他,让他好好看看。如果你觉得这篇文章还不错,请点个赞或者收个藏,点个关注更好啦。

本文内容有参考:

  1. 中文网址:将你的代码运行可视化
  2. 《流畅的Python》-- 第 8 章 对象引用、可变性和垃圾回收
  3. 《Python3标准库》-- 2.9 copy — Duplicate Objects

学习Python一年,这次终于弄懂了浅拷贝和深拷贝的更多相关文章

  1. 我终于弄懂了Python的装饰器(二)

    此系列文档: 1. 我终于弄懂了Python的装饰器(一) 2. 我终于弄懂了Python的装饰器(二) 3. 我终于弄懂了Python的装饰器(三) 4. 我终于弄懂了Python的装饰器(四) 二 ...

  2. 我终于弄懂了Python的装饰器(一)

    此系列文档: 1. 我终于弄懂了Python的装饰器(一) 2. 我终于弄懂了Python的装饰器(二) 3. 我终于弄懂了Python的装饰器(三) 4. 我终于弄懂了Python的装饰器(四) 一 ...

  3. 我终于弄懂了Python的装饰器(四)

    此系列文档: 1. 我终于弄懂了Python的装饰器(一) 2. 我终于弄懂了Python的装饰器(二) 3. 我终于弄懂了Python的装饰器(三) 4. 我终于弄懂了Python的装饰器(四) 四 ...

  4. Python——五分钟带你弄懂迭代器与生成器,夯实代码能力

    本文始发于个人公众号:TechFlow,原创不易,求个关注 今天是周一Python专题,给大家带来的是Python当中生成器和迭代器的使用. 我当初第一次学到迭代器和生成器的时候,并没有太在意,只是觉 ...

  5. [转]Hibernate与Jpa的关系,终于弄懂

    原文地址:http://blog.sina.com.cn/s/blog_5f1619e80100yoxz.html 我知道Jpa是一种规范,而Hibernate是它的一种实现.除了Hibernate, ...

  6. Hibernate与Jpa的关系,终于弄懂

    我知道Jpa是一种规范,而Hibernate是它的一种实现.除了Hibernate,还有EclipseLink(曾经的toplink),OpenJPA等可供选择,所以使用Jpa的一个好处是,可以更换实 ...

  7. 移动设备分辨率(终于弄懂了为什么移动端设计稿总是640px和750px)

    在我开始写移动端页面至今,一直有2个疑问困扰着我,我只知道结果但不知道为什么 问题1:为什么设计师给的设计稿总是640px或750px(现在一般以Phone6为基准,给的750px) 问题2:为什么我 ...

  8. 新知识:Java 利用itext填写pdf模板并导出(昨天奋战到深夜四点,知道今天两点终于弄懂)

    废话少说,不懂itext干啥用的直接去百度吧. ***************制作模板******************* 1.先用word做出界面 2.再转换成pdf格式 3.用Adobe Acr ...

  9. short s1 = 1; s1 = s1 + 1;和 short s1 = 1; s1 += 1;的问题,终于弄懂了

    对于short s1 = 1; s1 = s1 + 1; 由于s1+1运算时会自动提升表达式的类型,所以结果是int型,再赋值给short类型s1时,编译器将报告需要强制转换类型的错误. 对于shor ...

随机推荐

  1. spring基础:什么是框架,框架优势,spring优势,耦合内聚,什么是Ioc,IOC配置,set注入,第三方资源配置,综合案例spring整合mybatis实现

    知识点梳理 课堂讲义 1)Spring简介 1.1)什么是框架 源自于建筑学,隶属土木工程,后发展到软件工程领域 软件工程中框架的特点: 经过验证 具有一定功能 半成品 1.2)框架的优势 提高开发效 ...

  2. P1055_ISBN号码(JAVA语言)

    题目描述 每一本正式出版的图书都有一个ISBN号码与之对应,ISBN码包括9位数字.1位识别码和3位分隔符, 其规定格式如x-xxx-xxxxx-x,其中符号-就是分隔符(键盘上的减号), 最后一位是 ...

  3. java例题_22 用递归求阶乘 5!

    1 /*22 [程序 22 递归求阶乘] 2 题目:利用递归方法求 5!. 3 程序分析:递归公式:fn!=fn*4! 4 */ 5 6 /*分析 7 * 递归:如果其中每一步都要用到前一步或前几步的 ...

  4. PAT (Basic Level) Practice (中文)1078 字符串压缩与解压 (20 分) 凌宸1642

    PAT (Basic Level) Practice (中文)1078 字符串压缩与解压 (20 分) 凌宸1642 题目描述: 文本压缩有很多种方法,这里我们只考虑最简单的一种:把由相同字符组成的一 ...

  5. java面试-强引用、软引用、弱引用和幻象引用有什么区别

    在Java语言中,除了基本数据类型外,其他的都是指向各类对象的对象引用:Java中根据其生命周期的长短,将引用分为4类. 不同的引用类型,主要体现的是对象不同的可达性状态和对垃圾收集的影响. 1 .强 ...

  6. Dynamics CRM报表点击自动运行方法

    当我们点击了报表后一般会进入到条件筛选界面,再点击运行报表才可以直接运行报表.有一个方法可以点击报表后直接运行报表. 文本编辑器打开报表的rdl文件 找到如下位置的代码: 把Value部分改为: &l ...

  7. Go+gRPC-Gateway(V2) 微服务实战,小程序登录鉴权服务(四):客户端强类型约束,自动生成 API TS 类型定义

    系列 云原生 API 网关,gRPC-Gateway V2 初探 Go + gRPC-Gateway(V2) 构建微服务实战系列,小程序登录鉴权服务:第一篇 Go + gRPC-Gateway(V2) ...

  8. Linux就该这么学:重定向,管道符,通配符,转义符,环境变量

    第三章:重定向,管道符,环境变量 3.1 输入输出重定向 定义:输入重定向是指将文件导入命令中,输出重定向是指将原本显示到屏幕的信息输出问文件.相较于输入重定向,输出重定向使用的更多,下面重点介绍输出 ...

  9. 日志收集之filebeat使用介绍

    此系列文章一共分为三部分,分为filebeat部分,logstash部分,es部分.这里会按照每天几百亿条的数据量来考虑,去设计.部署.优化这个日志系统,来最大限度的利用资源,并达到一个最优的性能.本 ...

  10. 解决WebStorm无法正确识别Vue3组合式API的问题

    1 问题描述 Vue3的组合式API无法在WebStorm中正确识别,表现为defineComponent等无法被识别: 2 尝试方案 猜想这种问题的原因是无法正确识别对应的Vue3库,笔者相信Web ...