关于深浅拷贝,最直观的理解就是:
深拷贝:拷贝的程度深,自己新开辟了一块内存,将被拷贝内容全部拷贝过来了;
浅拷贝:拷贝的程度浅,只拷贝原数据的首地址,然后通过原数据的首地址,去获取内容。
这两者的优缺点对比:
深拷贝拷贝程度高,将原数据复制到新的内存空间中。改变拷贝后的内容不影响原数据内容。但是深拷贝耗时长,且占用内存空间。
浅拷贝拷贝程度低,只复制原数据的地址。其实是将副本的地址指向原数据地址。修改副本内容,是通过当前地址指向原数据地址,去修改。所以修改副本内容会影响到原数据内容。但是浅拷贝耗时短,占用内存空间少。
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,一定要注意这一点了!