美文网首页Python
学习Python一年,这次终于弄懂了浅拷贝和深拷贝

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

作者: 宇宙之一粟 | 来源:发表于2020-04-06 23:42 被阅读0次

    官方文档: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,以后在做数据的清洗、修改或者入库的时候,对原数据进行复制一份,以防数据修改之后,找不到原数据。

    1. 可以定制复制行为,通过实现__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一年,这次终于弄懂了浅拷贝和深拷贝

        本文链接:https://www.haomeiwen.com/subject/ryswphtx.html