深拷贝与浅拷贝

1. 可变对象与不可变对象

  在Python中,对象可以分为可变对象(Mutable Object)和不可变对象(Immutable Object)两种类型。可变对象指的是能够在原地修改的对象,即对象的值可以被改变而不需要创建新的对象。常见的可变对象包括列表(list)和字典(dict)。不可变对象指的是不能够被修改的对象,一旦创建后,它的值就不能再被改变,如果要修改它的值,只能创建一个新的对象。常见的不可变对象包括整数(int)、浮点数(float)、字符串(str0和元组(tuple)。

  下面是一些关于可变对象和不可变对象的特点:

  可变对象:

    可以被原地修改,不会改变对象的地址

    可以通过引用多次指向同一个对象

    可以作为字典的键或集合的元素

  不可变对象:

    不能被原地修改,操作会创建一个新的对象

    值的改变会导致创建一个新的对象,旧对象的地址会改变

    具有hash值,可以作为字典的键或集合的元素

  在python中比较两个对象常常用到= =与is

#可变对象列表
c=["123"]
d=["123"]
print(id(c))#2707103765000
print(id(d))#2707103765512
print(id(c[0]))#1287253524400
print(id(d[0]))#1287253524400

  上面的代码中,c的内存地址与d的内存地址不同,因为列表时可变对象,即使相同的两个列表内存地址也不同,使用id函数可以看作得到内存地址,其实是每个对象的唯一标志符。但是id(c[0])与id(d[0])是相同的,因为他们是字符串,字符串是不可变对象。id(c[0])是获取元素"123"的内存地址。对于字符串是不可变的,也就是"123"只有一份。在python3.1开始为了堆内存进行优化,针对不可变对象,提供了对象池(往后看,后面再提到)。对于上面的代码,其存储示意图如下:

  创建c=["123"]后,如下:

  再创建d=["123"]后,如下:

   如此时,给c列表添加一个元素,比如执行了c.append(1),则如下:

 

 

   假如现在我们要修改列表d的字符串'123'为"12",又该如何变化?由于字符串属于不可变对象,是无法被修改的,尽管我们执行了d[0]="12"确实可以执行,一般也是这么做的。但是实时上发生了下图所示的变化,d中的第一个位置的元素引用的是字符串"12"的地址,c中的"123"没有变。

  对于其他可变对象,使用id()函数得到的结果与列表原理差不多,这里暂时不详细讲述了。

  从python3.1开始:不可变数据拥有了对象池,其是一种内存优化机制。对于-5256的小整数。也就是说,当创建一个整数对象时,如果该整数的值在范围内,Python会尝试从整数对象池中获取已经存在的对象,而不是重新创建一个对象。这样做的好处是可以节省内存和创建对象的开销。实际上,在某些情况下,范围外的整数(不在[-5, 256]范围内)也可能指向同一个对象。这是因为在Python中,解释器可能会对一些整数对象进行缓存以提高性能,因此对于某些整数值,多个变量可能引用同一个对象。然而,这种行为是不可靠的,并且可能因Python解释器的不同版本、不同实现或不同的运行环境而有所不同。因此,我们不能依赖is操作符来判断两个整数对象是否相等。常使用==判断两个整数是否相同,一般没有使用is这样去做。
  看下面的整数代码:
#不可变对象
a=10
b=10
print(id(a))#140714195317296
print(id(b))#140714195317296

  不可变对象还包括元组,这里不举例子了。

2.引用赋值

  这里还是以列表为例子:

list01=["北京","上海"]
list02=list01 #引用赋值
print("list01",id(list01))#1678284182024
print("list02",id(list02))#1678284182024
# list01[0]="广东"
# list03=list01[:]
# list03[-1]="深圳"
# print("list03",id(list03))#1678284182536
# print(list01)#['广东', '上海']
# print(list02)#['广东', '上海']

  看上面的代码,绿色的部分故意注释了。list01与list02的id值相同,因为是采用的引用赋值,前面的c=["123"],d=["123"]是不同的。可以用下图来更加清晰的说明这个问题:

  再注视掉绿色的部分,运行后,结果如注释部分。我们使用 list03=list01[:]  与  list03[-1]="深圳"后,改变了list03,但是list02与list01并没有收到list03的影响,因为我们使用的是list03=list01[:] 赋值,这是采取切片的方式赋值,发生了浅拷贝,关于浅拷贝请继续往下看。

3.浅拷贝

  浅拷贝是一种创建一个新的对象,该对象引用原始对象的元素的副本的过程。浅拷贝只复制了原始对象中的元素的引用,而不是元素本身。

  这里还是以列表为例,看下面的代码:

#copy()浅拷贝
list1 = [1,[3, 4]]
list2 = list1.copy()
print(list1)#[1,[3, 4]]
print(list2)#[1,[3, 4]]
print(id(list1))#1741602956808
print(id(list2))#1741602956936

list1[0]=0
print(list1)#[0,[3, 4]]
print(list2)#[1,[3, 4]]

list1[1][0]=9
print(list1)#[0,[9, 4]]
print(list2)#[1,[9, 4]]

list1[1]=-1
print(list1)#[0,-1]
print(list2)#[1,[9, 4]]

分析如下:执行

list1 = [1,[3, 4]]
list2 = list1.copy()
之后,所发生改变的示意图如下:

   也就是说,如果列表里有元素是列表,则该位置存储了列表的地址。如图中所示。

  执行了list1[0]=0之后,示意图如下如所示:

   也就是说list1[0]=0,这一句实际上是将整数0在内存中的地址赋值给了list1所指向列表的第一个位置上的变量。而不是将1改为0,因为1是不可变对象,是不可能改变的,赋值页仅仅是改变了地址的指向。所以list2是没有变化的。

  执行 list1[1][0]=9之后,变化的示意图如下:

  也就是将9所在的地址赋值给了列表的第一个位置上的变量。所以输出list2时也跟着变化了。

  最后是执行了 list1[1]=-1这一句。示意图如下:

   可以从上面看出,经过一番操作后,list1与list2再也没有共同的元素了。如果想要list1第二个元素再次指向绿色部分的列表,可以将list2列表第二个位置的值(地址)赋值给它就可以了。

4.深拷贝

  深拷贝较为简单,数据完全独立,操作互不影响。就是比较费内存。

import copy

list_name=["北京",["上海","深圳"]]
data01=copy.deepcopy(list_name)#深拷贝
data01[0]="武汉"#互不影响
data01[1][0]="西安"#互不影响
print(list_name)#["北京",["上海","深圳"]]

   执行list_name=["北京",["上海","深圳"]]  data01=copy.deepcopy(list_name)#深拷贝代码后,其存储变化示意图如下:

 

  通过上图发现,与浅拷贝不同,深拷贝的data01的第二个元素是一个新的列表,不再和浅拷贝一样共用。

  执行data01[0]="武汉"   data01[1][0]="西安"这两句后,发生了如下变化:

   小结:文中大部分是以列表为例子进行讲解说明,对引用赋值,浅拷贝,深拷贝进行了比较详细的图解说明。浅拷贝除了文中提到的copy()外,切片也是浅拷贝,也可以使用copy.copy(要拷贝的对象)来实现浅拷贝,浅拷贝的本质是复制了原始对象中的元素的引用。深拷贝对于元素为列表时,不是复制了原列表元素的引用,而是重新分配了一个新的列表。

  

posted @ 2023-09-07 20:08  wancy  阅读(9)  评论(0编辑  收藏  举报