• 《Python进阶系列》二十九:append浅拷贝机制——你真的会用append函数吗?


    关于深浅拷贝,最直观的理解就是:

    • 深拷贝:拷贝的程度深,自己新开辟了一块内存,将被拷贝内容全部拷贝过来了;
    • 浅拷贝:拷贝的程度浅,只拷贝原数据的首地址,然后通过原数据的首地址,去获取内容。

    这两者的优缺点对比:

    • 深拷贝拷贝程度高,将原数据复制到新的内存空间中。改变拷贝后的内容不影响原数据内容。但是深拷贝耗时长,且占用内存空间
    • 浅拷贝拷贝程度低,只复制原数据的地址。其实是将副本的地址指向原数据地址。修改副本内容,是通过当前地址指向原数据地址,去修改。所以修改副本内容会影响到原数据内容。但是浅拷贝耗时短,占用内存空间少。

    Python内存引用

    在C语言中,在声明变量的时候,int aint b,这两条语句为ab两个变量分别赋予了两块不同的内存空间,然后赋值的时候再将相应的值存储到对应的存储空间。但是在Python中变量的赋值与C语言是截然不同的,考虑下面的代码:

    >>> a = 2
    >>> b = 2
    >>> id(a)
    140736259334576
    >>> id(b)
    140736259334576
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    id函数用于获取对象的内存地址,可以发现,变量a和变量b的内存地址竟然一样!

    在Python中,先生成对象,变量再对对象进行引用,在这个例子中,1就是对象,然后a再对1进行引用,由于常数是不可变类型,所以1的内存空间是一样的,所以ab引用的是用一块内存空间。虽然变量名不一样,但是他们引用的对象是相同的。
    在这里插入图片描述

    当然上面举的例子是int类型的,这属于不可变类型。如果是list或者dict呢?来看看下面的例子:

    >>> a = [1, 2, 3]
    >>> b = [1, 2, 3]
    >>> id(a)
    3145735383560
    >>> id(b)
    3145735414984
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    内存地址不一致!基于此,我们步入今天的主题,来看看append方法浅拷贝机制,到底有什么坑!

    append方法浅拷贝机制

    Python中的append方法是一个常用的方法,可以将一个对象添加到列表末尾。相信大家一定都用过吧?有人去深挖这个函数的用法吗?这里面可以存在一个大坑!

    我们来看一个例子:

    >>> a = [1, 3, 5, "a"]
    >>> b = []
    >>> b.append(a)
    >>> b
    [[1, 3, 5, 'a']]
    >>> a.append("aha")
    >>> b    # surprise?
    [[1, 3, 5, 'a', 'aha']]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    思考一下,明明在第三行之后并没有对b操作,那么为什么b会发生改变呢?

    回到今天的主题,事实上,append方法是浅拷贝。在Python中,对象赋值实际上是对象的引用,当创建一个对象,然后把它赋值给另一个变量的时候,Python并没有拷贝这个对象,而只是拷贝了这个对象的引用,这就是浅拷贝。

    我们逐步来看。首先,b.append(a)就是对a进行了浅拷贝,结果为b=[[1, 3, 5, 'a']],但b[0]a引用的对象是相同的,这可以通过id函数进行验证:

    >>> id(b[0])
    3145735177480
    >>> id(a)
    3145735177480
    
    • 1
    • 2
    • 3
    • 4

    可见,b[0]a指向同个内存地址。

    下一步,代码执行a.append(0),列表是可变类型,这一步在原地址的列表的末尾添加0,原地址的内容被改变了但是地址没有变(可以将Python中的list理解为链表,所以这个list的地址不会变,这相当于链表的头结点),所以ab[0]的内容同时被改变了,这就是为什么对a进行append操作b会跟着发生改变。
    在这里插入图片描述

    发生这些的前提是对同一个地址上的内容进行操作,所以影响了指向该地址的所有变量。

    所以,在日常使用append函数的时候,就需要将浅拷贝变为深拷贝,有两个解决方案:

    1. b.append(list(a))
    2. b.append(a[:])

    还是上面的例子,来看看这两个方法的结果是不是真的解决了append浅拷贝问题。

    >>> a = [1, 3, 5, "a"]
    >>> b = []
    >>> b.append(list(a))
    >>> b
    [[1, 3, 5, 'a']]
    >>> a.append(0)
    >>> a
    [1, 3, 5, 'a', 0]
    >>> b
    [[1, 3, 5, 'a']]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    >>> a = [1, 3, 5, "a"]
    >>> b = []
    >>> b.append(a[:])
    >>> b
    [[1, 3, 5, 'a']]
    >>> a.append(10)
    >>> a
    [1, 3, 5, 'a', 10]
    >>> b
    [[1, 3, 5, 'a']]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    怎么样,问题是不是解决了!所以日常使用中,一定要避免浅拷贝带来的问题!

    这个append的坑,也是我在刷leetcode:77. 组合时注意到的,题解为:

    class Solution:
        def combine(self, n: int, k: int) -> List[List[int]]:
            def traversal(n, k, start_index):
                if len(path) == k:
                    result.append(path[:])   # 精华在这,要解决这里的浅拷贝问题!
                    return
                
                for i in range(start_index, n + 1):
                    path.append(i)
                    traversal(n, k, i + 1)
                    path.pop()
            
            path = []
            result = []
            traversal(n, k, 1)
            return result
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    如果不处理第5行处的浅拷贝问题,会导致运行处下面的结果:
    在这里插入图片描述

    为啥?因为回溯呀,在上面代码的第11行处,一直在向上回溯,所以结果运行出来就变成了空列表!

    所以,在刷回溯的题的时候,如果你使用的是Python,一定要注意这一点了!

  • 相关阅读:
    微信小程序用户登录auth.code2Session接口开发
    shell编程之find/which/whereis/locate
    Android IO 框架 Okio 的实现原理,如何检测超时?
    IDEA 打开项目后看不到项目结构怎么办?
    【ML】机器学习数据集:sklearn中分类数据集介绍
    关于redis和mysql数据一致性的思考
    Spark 离线开发框架设计与实现
    枚举 小蓝的漆房
    【概率论基础进阶】随机变量的数字特征-矩、协方差和相关系数
    OCR基本原理
  • 原文地址:https://blog.csdn.net/qq_37085158/article/details/126849191