最近项目中遇到一个Python浅拷贝机制引起的bug,由于对于Python中对象引用、赋值、浅拷贝/深拷贝机制没有足够的认识,导致调试了很久才发现问题,这里简单记录一下相关概念。

在Python的设计哲学中,Python中的每一个东西都是对象,都有一个ob_refcnt变量,这个变量维护着对对象的引用计数,决定着对象的创建与消亡。

所以在Python程序中,一般的赋值操作其实只是将左值指向了右值的引用,并不会创建新的对象,可以通过id函数查看Python中对象在内存中的唯一标识,以list对象为例,如下代码:

>>> alist=[[1,2],3,4]
>>> blist=alist
>>> id(alist);id(blist) #alist/blist实际引用内存中的同一个list对象
140357688098184
140357688098184
>>> blist.append(5)
>>> blist
[[1, 2], 3, 4, 5]
>>> alist
[[1, 2], 3, 4, 5] #由于实际引用同一个list对象,blist增加一个元素后,alist的取值实际上是完全一样的
>>> id(alist);id(blist)
140357688098184
140357688098184

在上面的代码中,将alist的值赋给blist,其实只是把blist指向了alist在内存中的对象,两者引用了同一个list对象,此时如果对blist append一个新元素,由于是指向同一个对象,alist的输出结果一样会变化。

通过slice语法或者copy模块的copy函数,可以实现浅拷贝--

>>> import copy
>>> alist=[[1,2],3,4]
>>> blist=alist[:]
>>> clist=copy.copy(alist)
>>> id(alist);id(blist);id(clist) #alist/blist/clist实际已经指向内存中的不同list对象
140357691858696
140357691897864
140357720939912
>>> id(alist[0]);id(blist[0]);id(clist[0]) #alist[0]/blist[0]/clist[0]三个子对象依然指向内存中的同一个list对象
140357691897800
140357691897800
140357691897800
>>> blist.append(5)
>>> blist
[[1, 2], 3, 4, 5]
>>> alist
[[1, 2], 3, 4] #blist对象值的变更,不会再影响到alist和clist
>>> clist
[[1, 2], 3, 4]
>>> alist[0].append('a')
>>> alist
[[1, 2, 'a'], 3, 4]
>>> blist
[[1, 2, 'a'], 3, 4, 5] #由于实际引用同一对象,alist[0]子对象值的变更,也会从blist[0]/clist[0]上体现出来
>>> clist
[[1, 2, 'a'], 3, 4]
>>> id(alist[1]);id(blist[1]);id(clist[1])
10919488
10919488
10919488

可以看到blist和clist本身已经是新的list对象,不再引用alist这个list对象,但是三个list中的子对象还是相同的引用,因为python中的浅拷贝只能拷贝父对象,不会拷贝对象内部的子对象。

通过copy模块中的copy.deepcopy函数可以实现深拷贝:

>>> alist=[[1,2],3,4]
>>> blist=copy.deepcopy(alist)
>>> id(alist);id(blist) #alist/blist已经引用内存中不同的list对象
140357692023560
140357691897608
>>> blist.append(5)
>>> blist
[[1, 2], 3, 4, 5]
>>> alist
[[1, 2], 3, 4] #blist取值的变更,不会影响到alist
>>> id(alist[0]);id(blist[0]) #alist{0]/blist[0]两个子对象也已经引用内存中不同的list对象
140357691897864
140357691896136
>>> alist[0].append('a')
>>> alist
[[1, 2, 'a'], 3, 4]
>>> blist
[[1, 2], 3, 4, 5] # alist[0]子对象值的变更,也不会再印象到blist[0]的值
>>> id(alist[1]);id(blist[1])
10919488
10919488

可以看到,通过copy.deepcopy进行拷贝后,alist和blist指向不同的list对象,同时其子对象alist[0]/blist[0]也指向了不同的list对象,但是alist[1]/blist[1]还是指向相同的对象,这是因为3、4在Python中其实是不可变对象,相当于是常量,在Python中不可变对象只会存在唯一一份,所以无论浅拷贝/深拷贝,都是对同一个不可变对象进行的引用。

对于dict/set这些Python类型对象的赋值操作,也会存在类似的浅拷贝/深拷贝的问题,下面再以dict为例贴一下代码:

引用赋值:

>>> adct={'d':{1:2}, 3:4}
>>> bdct=adct
>>> id(adct);id(bdct) #adct/bdct实际引用内存中的同一个dict对象
140357688090760
140357688090760
>>> id(adct['d']);id(bdct['d']) #adct['d']/bdct['d']两个子对象实际引用内存中的同一个dict对象
140357691897928
140357691897928
>>> bdct['d'].update({'a':'b'})
>>> bdct
{'d': {1: 2, 'a': 'b'}, 3: 4}
>>> adct
{'d': {1: 2, 'a': 'b'}, 3: 4} #由于实际指向同一个子对象,bdct['d']取值的变更会直接影响到adct的值

copy.copy浅拷贝:

>>> adct={'d':{1:2}, 3:4}
>>> bdct=copy.copy(adct)
>>> id(adct);id(bdct) #adct/bdct引用不同的dict对象
140357688082888
140357720937544
>>> id(adct['d']);id(bdct['d']) #adct['d']/bdct['d']两个子对象依然指向内存中同一个dict对象
140357688101704
140357688101704
>>> bdct['d'].update({'a':'b'})
>>> bdct
{'d': {1: 2, 'a': 'b'}, 3: 4}
>>> adct
{'d': {1: 2, 'a': 'b'}, 3: 4} #由于实际引用同一个子对象,bdct['d']子对象值的变更会直接影响到adct的值

copy.deepcopy深拷贝:

>>> adct={'d':{1:2}, 3:4}
>>> bdct=copy.deepcopy(adct)
>>> id(adct);id(bdct) #adct/bdct本身已经引用不同的dict对象
140357691897928
140357688094152
>>> id(adct['d']);id(bdct['d']) #adct/bdct的子对象引用了不同的dict子对象
140357688090760
140357688085896
>>> bdct['d'].update({'a':'b'})
>>> bdct
{'d': {1: 2, 'a': 'b'}, 3: 4}
>>> adct
{'d': {1: 2}, 3: 4} #bdct['d']子对象的变更不会再影响到adct['d']的值

Python中的对象引用、浅拷贝与深拷贝的更多相关文章

  1. 图解python中赋值、浅拷贝、深拷贝的区别

    Python中,对象的赋值,拷贝(深/浅拷贝)之间是有差异的,如果使用的时候不注意,就可能产生意外的结果.下面本文就通过简单的例子介绍一下这些概念之间的差别. 对象赋值 直接看一段代码: will = ...

  2. Python中赋值、浅拷贝与深拷贝

    python中关于对象复制有三种类型的使用方式,赋值.浅拷贝与深拷贝.他们既有区别又有联系,刚好最近碰到这一类的问题,研究下. 一.赋值 在python中,对象的赋值就是简单的对象引用,这点和C++不 ...

  3. 关于python中赋值、浅拷贝、深拷贝之间区别的深入分析

    当重新学习了计算机基础课程<数据结构和算法分析>后再来看这篇自己以前写的博文,发现错误百出.python内置数据类型之所以会有这些特性,归根结底是它采用的是传递内存地址的方式,而不是传递真 ...

  4. python中赋值、浅拷贝、深拷贝详解(转)

    一.赋值 >>> a = [1, 2, 3]>>> b = a>>> print(id(a), id(b), sep='\n')139701469 ...

  5. python中赋值和浅拷贝与深拷贝

    初学编程的小伙伴都会对于深浅拷贝的用法有些疑问,今天我们就结合python变量存储的特性从内存的角度来谈一谈赋值和深浅拷贝~~~ 预备知识一——python的变量及其存储 在详细的了解python中赋 ...

  6. Python中赋值、浅拷贝和深拷贝的区别

    前言文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理. PS:如有需要Python学习资料的小伙伴可以加点击下方链接自行获取http: ...

  7. Python中list的复制及深拷贝与浅拷贝探究

    在Python中,经常要对一个list进行复制.对于复制,自然的就有深拷贝与浅拷贝问题.深拷贝与浅拷贝的区别在于,当从原本的list复制出新的list之后,修改其中的任意一个是否会对另一个造成影响,即 ...

  8. 一入python深似海--浅拷贝与深拷贝

    python中有一个模块copy,deepcopy函数用于深拷贝,copy函数用于浅拷贝. 要理解浅拷贝,必须先弄清楚python中的引用. 引用 Python中一切都是对象,变量中存放的是对象的引用 ...

  9. Python对象赋值、浅拷贝、深拷贝

    Python中,基本数据类型,理解为常见数据类型:布尔型.整型.浮点型.字符串.列表.元组.字典.集合,随语言不同而不同,但是根据在内存中存储方式的不同,区分开原子类型和容器类型. 对象赋值 对象的赋 ...

随机推荐

  1. IOS http(上传和下载)

    HttpTool.h #import <Foundation/Foundation.h> typedef void (^HttpToolProgressBlock)(CGFloat pro ...

  2. Java虚拟机13:Java类加载机制

    前言 我们知道我们写的程序经过编译后成为了.class文件,.class文件中描述了类的各种信息,最终都需要加载到虚拟机之后才能运行和使用.而虚拟机如何加载这些.class文件?.class文件的信息 ...

  3. git 如何忽略文件以及使用.gitignore 不生效的解决办法

    (1) git 如何忽略文件 在git中如果想忽略掉某个文件,不让这个文件提交到版本库中,可以使用修改根目录中 .gitignore 文件的方法(如无,则需自己手工建立此文件).这个文件每一行保存了一 ...

  4. Hadoop学习之路(二十八)MapReduce的API使用(五)

    求所有两两用户之间的共同好友 数据格式 A:B,C,D,F,E,O B:A,C,E,K C:F,A,D,I D:A,E,F,L E:B,C,D,M,L F:A,B,C,D,E,O,M G:A,C,D, ...

  5. Qt中QScrollArea类的简单使用心得

           平台:windows 64位        Qt版本:5.5.1 MinGW 32bit 根据自己目前的需求简单说下怎么在QScrollArea滚动窗口中实现多个控件的滚动显示,先看看最 ...

  6. PHP整数转小数

    sprintf('%.2f',$memrow['yjrebate1']+$memrow['yjrebate2'])

  7. 【luogu P3953 逛公园】 题解

    题目链接:https://www.luogu.org/problemnew/show/P3953 题外话:感觉2017年神题好多..这还不是最神的一道,真在考场上我也就写个最短路计数暴力了.现在在大佬 ...

  8. cloudstack 用admin 账号创建虚拟机只是提示insufficient resource

    报错: com.cloud.exception.ResourceUnavailableException: Resource [DataCenter:1] is unreachable: Unable ...

  9. nRF5 SDK for Mesh(四) 源码编译

    官方文档教程编译源码: http://infocenter.nordicsemi.com/index.jsp?topic=%2Fcom.nordic.infocenter.sdk%2Fdita%2Fs ...

  10. 处理HTML表单(11)

    PHP和Web表单 <?php if(isset($_POST["name"])){//isset()函数设置变量是否设置,并且不能为空 $name = $_POST[&qu ...