Python中append浅拷贝机制详解
程序猿-张益达 人气:0Python中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 ) )
加载全部内容