Python 引用机制与深浅拷贝深度解析
本文深入解析Python中的引用机制,通过具体案例阐明变量赋值、列表操作对内存地址的影响。详细对比浅拷贝与深拷贝的区别,介绍copy模块的使用方法及不可变类型的处理逻辑,帮助开发者避免常见的数据修改陷阱。
引言与问题背景
近期在开发中遇到了一些关于数据复制的问题,这让我意识到如果长时间不复习基础知识,很容易出现遗忘。因此,我决定整理这份笔记,重新梳理相关概念。
前续
首先简要描述遇到的具体问题:题目要求预测两个print语句的输出结果。

观察代码可知,变量a指向了一个列表(list)对象。在Python中,这种赋值语句的内部含义是a指向该list所在的内存地址,这可以类比为指针的概念。
对于变量b,需要注意的是,它将a对象包裹进一个新的列表中,并执行了乘以5的操作。因此,b的结构是一个包含5个元素的大列表,每个元素都是对a的引用。
当对a对象执行append操作时,实际上修改的是内存中该list对象的内容。由于所有指向该对象的引用都会同步反映这一变化,因此b中的元素也会随之改变。
为了验证这一点,我打印了a的id,同时也打印了b列表中每个元素(即a)的id。结果显示,b中每个元素的id与a的id完全一致。

从输出可以看到,a对象的id(内存地址)为10892296。虽然b将a包裹进了一个新的列表结构中,但b中的元素引用的仍然是相同的内存地址对象,具体关系可参考下图解释。

随后,我们对a进行了append操作。由于list是可变对象,其内存地址并未改变,但内存中该地址所指向的内容发生了变化。这导致所有引用该地址的对象都受到了影响,如上图分割线下半部分所示。
基于上述现象,我们进一步复习Python的引用机制、浅复制(Shallow Copy)及深复制(Deep Copy)的相关知识。
Python引用机制解析
Python的引用机制
引用机制案例1
通过上述例子可以看出,Python的引用传递机制最终使得两个变量都引用了内存中同一块区域的内容。
接下来我们看另一个例子。
变量B通过变量A,同样引用了id为17446024的地址内容,两者的id(内存地址)完全相同。
因此,通过A执行A[0]=3或A[3].append(6)操作,都会修改这块内存中的内容(因为list是可变对象,其内存地址保持不变,这一点稍后详述)。
这是最基本的引用案例(补充说明:由于A和B指向同一内存地址,通过B修改的内容同样会反映在A上)。

引用机制案例2
我们再来看一个案例。
从题目表面看,似乎是将元素2替换为列表本身,结果可能是A=[1,[1,2,3],3]。
但实际上并非如此!你可以看到,红框部分显示中间存在无限多个嵌套结构。
为什么会这样呢?
原因在于,A指向的是[1,2,3]这个列表。在此例中,只是将A的第2个元素指向了A对象本身,这意味着A的结构发生了变化,但A仍然指向原来的对象。
我们可以通过打印A的id来验证,其指向并未改变。

来看一下,A的指向并没有变。

那么,如果要达到最终输出效果为[1,[1,2,3],3],应该如何操作呢?
这里,我们需要用到浅复制,用法如下。

浅复制与深复制详解
浅复制和深复制
浅复制
现在,我们来讨论浅复制和深复制。上述方法实际上只是进行了浅复制(shallow copy),其含义是对原来引用的对象进行了复制,但不再引用同一对象地址。
看下面的例子,B通过B = A[:]操作进行了浅复制。你可以看到,浅复制之后,A和B引用的内存地址已经不同。但是,A和B内部元素的引用地址仍然相同,这点需要特别注意,二者是有区别的。
A和B引用内存地址的不同带来的效果是,在B上进行的操作不会影响A。

浅拷贝归纳:
因此,浅拷贝可以归纳为:复制一份引用,新对象和原对象的顶层引用被区分开,但内部元素的地址引用仍然相同。
然而,浅复制也存在一个问题,即在遇到嵌套情况时。如下所示,我给B赋值了一份A的浅复制,这样A和B的id(内存地址)就不同了。
因此,当我修改A[0]=8时,B不会受到影响,因为A和B两者是独立的引用。但是,中间有一个嵌套列表[4,5,6]。这个[4,5,6]我们可以理解为:A和B仍然共同引用着,即对于A和B的第二个元素来说,它们仍指向同一块内存地址。
另外需要说明的是,由于int是不可变类型,所以将A[0]修改为8之后,其引用地址就变了,从而与B[0]这个元素的引用区分开了。

深复制
那么如何面对这样的情况呢?这就需要用到Python模块中的copy模块。
copy模块有两个功能。
1: copy.copy(你要复制的对象):这是浅拷贝,与前面list进行的[:]操作性质相同。
2: copy.deepcopy(你要复制的对象):这是深拷贝,它除了像浅拷贝一样新生成一份对象的引用外,对于内部的所有元素,都会新生成引用,以实现完全独立。
看下面的例子,当你给B赋值一份A的深复制之后,二者可以说是完全独立了。无论修改的是A里面的不可变元素,还是修改A里面嵌套的可变元素,结果都不会影响到B。
我的理解是:深复制可以称之为递归拷贝,它会将所有嵌套的可变元素都拷贝一遍,然后独立引用出来。

深复制归纳:
深复制的效果,除了和浅复制一样将对象的引用新生成一份外,对于内部所有嵌套的元素,它都会帮你一一独立开。
自己画了两张图,以表示浅复制和深复制的效果区别。
需要说明的是,虽然浅复制之后,列表内不可变元素的引用地址还是相同的,但正因为它们是可变元素,所以其中任意不可变元素被改变之后,引用地址都会是新的,而不会影响到原来的引用地址。


总结与特殊类型处理
总结
至此,浅复制和深复制的机制基本理解完毕。
另外还有特殊情况需要说明。
对于不可变类型:int, str, tuple, float等元素来说,不存在拷贝的概念。它们被修改之后,引用地址会直接改变,如下面所示。

但是,如果不可变类型内部有嵌套的可变类型时,还是可以使用深复制的。

另外要提醒一句,平时我们用的最多的直接赋值(或者说是直接传递引用)的方法,如下面例子所示。
它是将a和b两个可变元素同时指向一个内存地址,因此,任何改变都会波及到a和b。

最后
可变类型:list , set , dict
不可变类型:int, str , float , tuple
浅复制方法:[:] , copy.copy() , 使用工厂函数(list/dir/set)
深复制方法:copy.deepcopy()
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









