Python中append浅拷贝机制详解

软件发布|下载排行|最新软件

当前位置:首页IT学院IT技术

Python中append浅拷贝机制详解

程序猿-张益达   2023-03-20 我要评论

Python中append浅拷贝机制

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

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

这两者的优缺点对比:

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

Python内存引用

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

>>> a = 2
>>> b = 2
>>> id(a)
140736259334576
>>> id(b)
140736259334576

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

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

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

>>> a = [1, 2, 3]
>>> b = [1, 2, 3]
>>> id(a)
3145735383560
>>> id(b)
3145735414984

内存地址不一致!基于此,我们步入今天的主题,来看看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']]

思考一下,明明在第三行之后并没有对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

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

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

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

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

  • b.append(list(a))
  • 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']]
>>> 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']]

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

这个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

如果不处理第5行处的浅拷贝问题,会导致运行处下面的结果:

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

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

补充:Python append() 与深拷贝、浅拷贝

深浅拷贝

在 Python 中,对象赋值实际上是对象的引用。当创建一个对象,然后把它赋给另一个变量的时候,Python 并没有拷贝这个对象,而只是拷贝了这个对象的引用,我们称之为浅拷贝。

在 Python 中,为了使当进行赋值操作时,两个变量互补影响,可以使用 copy 模块中的 deepcopy 方法,称之为深拷贝。

append() 函数

当 list 类型的对象进行 append 操作时,实际上追加的是该对象的引用。

id() 函数:返回对象的唯一标识,可以类比成该对象在内存中的地址。

>>>alist = []
>>> num = [2]
>>> alist.append( num )
>>> id( num ) == id( alist[0] )
True

如上例所示,当 num 发生变化时(前提是 id(num) 不发生变化),alist 的内容随之会发生变化。往往会带来意想不到的后果,想避免这种情况,可以采用深拷贝解决:

alist.append( copy.deepcopy( num ) )

Copyright 2022 版权所有 软件发布 访问手机版

声明:所有软件和文章来自软件开发商或者作者 如有异议 请与本站联系 联系我们