Python 直接赋值、深拷贝、浅拷贝的详解

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/zsq8187/article/details/109907066

浅拷贝与深拷贝总览

Python 中对象的赋值是直接通过传递引用进行的。需要进行拷贝则需使用标准库中的 copy 模块。

  • 直接赋值:直接传递内存地址

  • 浅拷贝 copy.copy()创建新对象,里面复制所有元素的内存地址

  • 深拷贝 copy.deepcopy()创建新对象,不可变的复制内存地址,含可变的就递归创建新对象并复制内容(“含可变”表示可变类型以及元组内含可变类型元素,见下方)

    • 在深拷贝这里,不会因为元组是不可变类型而不检测(实际上判断的依据应该是是否为容器类型,但是为了方便记忆,我们可以记可变、不可变),而会进一步检测元组里面是否包含可变类型的元素而进行复制。如果元组里有可变元素,则会复制出新的元组!
    import copy
    
    a = (1, 2)
    print(id(a) == id(copy.deepcopy(a)))  # True
    
    a = (1, [2])
    print(id(a) == id(copy.deepcopy(a)))  # False
    
    • 为什么不可变的是复制内存地址呢🤔?看下小节[不可变类型的“拷贝”]就明白了🧐。
  • 不可变类型:Number(数字)、String(字符串)、Tuple(元组)【除元组外都是非容器类型】
  • 可变类型:List(列表)、Dictionary(字典)、Set(集合)【都是容器类型】
  • 非容器类型:Number(数字)、String(字符串)【都是不可变类型】
  • 容器类型:List(列表)、Dictionary(字典)、Tuple(元组)【除元组外都是可变类型】

不可变类型的“拷贝”

不可变类型除元组外都是非容器类型,它们没有拷贝一说(详见下方)。使用 copy 模块进行拷贝的话都是直接赋值,传递内存地址。

  • 理解 copy 模块的一些设计原因,首先要了解这个直接赋值而实现“拷贝”的原因。
  • 因为拷贝的需求就是希望两个变量互不干扰,要新的一份。而不可变类型的“修改”就已经实现了两者的独立性(它的“修改”都是重新创建一个变量,将新变量指向新对象,旧变量依旧指向旧对象)。因此拷贝内容也就没必要再复制开辟新空间,新旧变量均指向同一个地址即可。

Python-不可变类型的修改

  • 图中的验证:
a = "a"
b = a  # 直接赋值,进行“拷贝”
print(b is a)
b = "b"  # “修改”又是直接赋值了,因为它们无法修改
print(a)
print(b)
print(b is a)
True
a
b
False
  • 不可变类型的元组也是直接赋值。但是含有可变元素的元组特殊,它的拷贝就必须考虑到内部可变元素,详见下一小节。

—> 所以在下面的可变类型/容器类型的拷贝中,有关不可变类型的子元素,就直接赋值传递内存地址即可。

要注意的是,这里直接赋值是将新旧两个变量标签直接指向同一个对象。“修改”是针对一个变量标签“修改”,没有动到另一个变量标签。从而两者独立不影响。


可变类型的直接赋值

上面笔者讲了不可变类型的直接赋值,这里来偏个题看看可变类型的直接赋值。这个就是变量标签指向同一个对象。一直都是操作同一个对象,所以显而易见没有达到拷贝的需求:
容器类型的直接赋值_20201121122656

(图片下面的小标题有误,不想编辑了,意思懂了就行,容器类型里的元组是比较特殊的)


可变类型的深拷贝与浅拷贝

它的深浅拷贝首先均会【创建一个新对象】,但是直接子元素的指向有所不同。这里是将新旧两个变量标签指向了不同的两个对象!(元组特殊,后面注意)

  • 所以日后对这两个变量的修改是互不干扰的!这也可以从上面的“不可变类型的直接赋值”图解简易类推:变量 AB 替换成对象 AB(它们一一对应嘛);这两个对象是容器,指向很多“字符串”。
import copy
aList = ["你", "好", 1, [1, 2, 3]]
bList = copy.copy(aList)  # 浅拷贝:拷贝了所有元素的内存地址到一个新list对象里
cList = copy.deepcopy(aList)  # 深拷贝:在浅拷贝的基础上,还递归为容器元素创建新对象拷贝内容

print(id(aList[3]))  # 2249103153728
print(id(bList[3]))  # 2249103153728
print(id(cList[3]))  # 2249103153216
# 可以看到,浅拷贝里,都是相同的内存地址;而深拷贝会将可变类型子元素再次拷贝!

在直接赋值里,我们以新旧两个变量标签为基准,来看变量内容的修改。

而这里,我们继续看变量标签,它指向了新旧两个容器对象。当容器对象的元素为不可变类型时,遵循上述的逻辑,新建对象赋予容器对象的元素新的地址从而互不影响。

可变类型的浅拷贝

可变类型的浅拷贝中,因为不可变类型 "你""好"1 修改都是新建对象赋予新地址,从而两者互不干扰。

但是修改子列表 [1, 2, 3] 不会创建新对象,因此两者这个一直都是引用同一个,互有影响。
浅拷贝_20201121120720

  • 字典中的浅拷贝:创建两个字典对象,对键创建了新的对象*,但是复制了值的内存地址,它们值是共享的(需要了解字典的内存示意图)。如果值有可变类型,那么依旧互有影响。
    • 对键创建了新的对象:例如有字典 A、B:B 是 A 的浅拷贝,此时如果删除 B 的一个键,那么 A 中对应的键是没有影响的。实现了两个字典的这种初步独立,实现了这种浅拷贝的需求。
    • 详细图解见[附:字典的深浅拷贝]。

可变类型的深拷贝

可变类型的深拷贝中,进行了递归,为容器元素创建了新对象拷贝内容。从而实现两个变量永远互不干扰。
深拷贝_20201121120733

  • 字典中的深拷贝:对键、值都创建了新的对象(不可变类型的依旧只是复制内存地址),两者互不干扰。

元组的深浅拷贝

  • 元组是不可变类型,应该是不会进行复制新对象的,是传递内存地址

  • 浅拷贝:元组的浅拷贝实际上是直接传递内存地址。

  • 深拷贝:特殊的是含可变类型元素的元组的深拷贝会进行复制:新建一个元组对象,并且递归为可变元素复制创建新对象。

    只有这样,深拷贝出来的元组对象与原元组对象才能互相独立。

附:字典的深浅拷贝

理解了上面图解列表的深浅拷贝后,再类推理解字典的深浅拷贝也就不难了。

字典的浅拷贝_20201121193753
字典的深拷贝_20201121195901

  • 注意到这里面,无论深浅拷贝均复制了键对象哦!

有关上述的测试:略。

转载资料

理解上文所说的后,对照看一下这篇文章里的几张图,看看与自己想的是不是一样的。

  1. 不可变元组 和 可变列表的深拷贝,浅拷贝区别

不可变元组 和 可变列表的深拷贝,浅拷贝区别_20201121201245

这里,第一部分,全为不可变子元素的元组是直接赋值,所以都是 True
第二部分,拷贝可变类型,浅拷贝和深拷贝它们的内容是一致的所以 == 判断时为 True;而它们拷贝都是新建对象,所以内存地址 == 判断为 False

如果你不能自己看懂此题,说明你还没有真正的理解本文所说的,建议复读几遍。

拷贝的使用处

Python 如果使用到复制的,一般默认使用的是浅拷贝。以下例子等待继续学习补充。

  • 函数的参数传递是传递地址,没有拷贝!

比如:切片的 [:] 使用的是浅拷贝、

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值