直接赋值、浅拷贝、深拷贝

在 Python 中,一切皆对象,变量本质上保存的是对象的引用。因此在理解复制行为时,需要区分“对象本身”和“对象引用”。

直接赋值

两个变量指向同一个对象,修改其中一个变量的值会影响另一个变量的值。

在内存中的逻辑图示: 直接赋值前


修改其中一个变量的值:

修改之后的内存中的逻辑图示: 直接赋值后

这里我们需要注意的是,虽然我们修改了nums2的值,但是nums1的值也被修改了。这是因为变量nums1和变量nums2指向同一个实例对象(可变对象),因此它们看到的列表内容也是一样的。但是,list 里的元素是 int 类型(不可变对象)。当我们执行 nums2[3] = 99 时,并不会创建新的 list,而是把 list 中第 3 个位置原本指向 40 的引用,替换为指向 99。由于 nums1nums2 指向的是同一个 list 对象,所以通过 nums2 修改 list 后,nums1 看到的内容也会发生变化。 另外需要注意,因为 list 是可变类型,所以在对 list 进行元素修改时,通常不会创建新的 list 对象,而是直接在原来的 list 对象上进行修改。(这里只是简单说明可变类型和不可变类型的区别)


浅拷贝

浅拷贝是创建一个新的对象,但它包含的元素仍然是对原始对象中元素的引用。如果元素是可变对象,修改其中一个对象内部的元素,可能会影响另一个对象。

在内存中的逻辑图示: 浅拷贝修改对象之前

修改之后的内存中的逻辑图示: 浅拷贝修改对象之后

我们可以看出,虽然 nums1nums2 是两个不同的列表对象(通过浅拷贝,它们的地址不同),但是它们里面的元素是相同的(它们的元素地址相同)。当我们修改 nums2[3] 的值时,实际上是修改了 nums2 列表中的一个元素,而这个元素在内存中是一个不可变对象(整数),所以会创建一个新的整数对象 99,并将 nums2[3] 的引用指向这个新的对象。由于 nums1[3] 仍然指向原来的整数对象 40,所以 nums1 中的元素没有发生变化。

那我们就能引发一个问题:上面我们想修改nums2[3]元素的值,我们发现它是int类型的数据,是不可变的,才有了上面的效果,那么如果 nums1nums2 中的元素是可变对象(比如列表),那么修改其中一个对象的元素会不会影响另一个对象呢?

这就引出了深拷贝的概念。


深拷贝

深拷贝会创建一个 新的对象,并且 递归复制原对象中的所有子对象。

在内存中的逻辑图示: 深拷贝修改对象之前

在修改之后的内存中的逻辑图示: 深拷贝修改对象之后

通过上面的代码,我们可以看出,nums1nums2 是两个不同的列表对象(通过深拷贝,它们的地址不同),并且其中的可变子对象也会被复制,因此它们引用的子对象地址也不同。当我们修改 nums2[3][0] 的值时,实际上是修改了 nums2 列表中的一个元素(一个列表),由于这个元素在内存中是一个可变对象(列表),所以会直接修改这个列表对象中的元素,而不会创建新的对象。由于 nums1[3] 仍然指向原来的列表对象,所以 nums1 中的元素没有发生变化。


拷贝的特殊情况

  1. 非容器类型(如数字、字符串、和其他“原子”类型的对象)无法拷贝

  2. 元组变量如果只包含原子类型对象,则不能对其深拷贝


在实际编程过程中,我们其实一直都在接触“拷贝”相关的概念。例如在函数传参时,Python 传递的本质是 对象的引用(也可以理解为对象引用的拷贝)。

如果参数是 可变对象(例如 list、dict 等),在函数内部对对象内容进行修改时,可能会影响函数外部的原对象。

如果参数是 不可变对象(例如 int、str、tuple 等),由于对象本身不可修改,在函数内部进行“修改”时实际上会创建新的对象,因此不会影响外部变量。

因此,在处理可变对象时,如果不希望函数内部的操作影响原始数据,就需要考虑是否使用 浅拷贝或深拷贝 来创建独立的数据副本。

总体来说,Python 中变量保存的是对象的引用,而不是对象本身。因此理解“引用关系”和“对象是否可变”,是理解直接赋值、浅拷贝和深拷贝的关键。(:D 可以看看切片他的底层是什么实现的嘞~)