• Python中的对象引用、浅拷贝与深拷贝


    最近项目中遇到一个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']的值
    签名:新加入开源大部队的新手程序员,拥抱开源,拥抱自由
  • 相关阅读:
    在 Tomcat 8 部署多端口项目
    tar -zxvf jdk-8u144-linux-x64.tar.gz
    linux下删除文件夹的命令
    springboot+mybatis案例
    阿里云主机密码
    查看公钥
    jenkins安装
    redis详解(包含使用场景)
    什么是JSONP?
    在CentOS7上面搭建GitLab服务器
  • 原文地址:https://www.cnblogs.com/AcAc-t/p/python_object_ref_shallow_deep_copy.html
Copyright © 2020-2023  润新知