Python中append浅拷贝机制详解
作者:程序猿-张益达
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 ) )
到此这篇关于Python中append浅拷贝机制的文章就介绍到这了,更多相关Python中append浅拷贝内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!