python的赋值、指向、浅拷贝、深拷贝

1 篇文章 0 订阅
1 篇文章 0 订阅

2020年12月10日,关于浅拷贝和深拷贝

整理了一下在python中关于浅拷贝和深拷贝的知识点,已做记录
准备工作:

  • 数据格式:int、string、list、tuple、set、dict
  • 数据类型:可变类型(列表、集合、字典)、不可变类型(整型、字符串、元组)
  • 方式:赋值、指向、浅拷贝、深拷贝
  • 内存空间:即id值,每个数据都可以通过id()方法查看该数据在内存中存储的位置
  • 变量名:(规范化)默认由数字字母下划线组成,不要与python关键字重复,且以字母开头

1.赋值和指向

在python中,如果执行:

>>> a = 1
>>> b = [1,2]
>>> c = {'a':1,'b':2}
>>> id(a)
94250626806560
>>> id(b)
140531868012296
>>> id(c)
140531869644048

那么,python执行时,系统会立即在内存中开辟一个空间,用来存储1,[1,2]和{‘a’:1,‘b’:2}这三个数据,上述空间会用一个"标记"来标记这个内存空间以便寻找,即id值。就好比每个家庭的住址,可以通过唯一住址找到这家的人,例如上述代码中的a,b和c中,我们可以使用id(a),id(b),id©查看它们的地址,可以通过这个id值找到我们定义的数据类型存储的位置。

这里需要指出变量名只是指向了上述id地址,并不是所定义的1,[1,2]或{‘a’:1,‘b’:2}这三个数据,我们可以通过a去寻找到1,可以通过b寻找到[1,2],可以通过c去寻找到{‘a’:1,‘b’:2}。
在这里插入图片描述

2.如何判断是否可变

对于列表、集合和字典,在对它进行修改时,例如通过列表的append,集合的add,字典的update等众多修改方法,虽然列表、集合和字典的"值"的确有变化,但是他们各自的内存地址不会发生变化,即:在可变数据类型的常规修改前后,id没有发生变化,代码如下:

>>> list1 = [1,2,3]
>>> id(list1)
140531867966280
>>> list1.append(4)
>>> list1
[1, 2, 3, 4]
>>> id(list1)
140531867966280

注意id,在修改前后没有发生变化,所以可以理解为:当创建list1时,电脑的内存中开辟了一个空间(即开辟了一个id),这个空间(id)是专门用来存放[1,2,3]这个数据的,所谓可变数据类型,就是在这个内存空间(id)中,操作者可以任意修改list1的值,或者说,系统允许这个内存空间(id)中的数据发生变化,所以列表被称为可变数据类型,集合和字典同理。

题外话:后来我尝试了如果给两个不同的变量名赋值看似相同的值,那么数据类型可变或者不可变是否会有影响,测试下:

>>> a = [1,2]
>>> b = [1,2]
>>> id(a) == id(b)
False

输出为False,说明a和b虽然数值相同,但不是存储在同一个内存空间中(id),因为这是必然的,不然的话就乱套了,如果a和b指向的同一块内存(id),那么a改变,b也跟着改变,这是不可能的,所以上面代码定义a和b之后,内存是开辟两个空间,分别存储了[1,2]和[1,2],两个不同的空间id肯定不一样,同理,列表,集合,字典,元组在分别赋值给不同的变量名时也是不同的id,但是int类型和string类型就是相同的,例如:

>>> a = 1
>>> b = 1
>>> id(a)
94279789044512
>>> id(b)
94279789044512
>>> str1 = 'small'
>>> str2 = 'small'
>>> id(str1)
140664892556824
>>> id(str2)
140664892556824

可以发现,不同的变量名赋值相同数据的整型或者字符串时,id相同,也就是说,对于同一个数字或一个字符串,系统只会开辟一次空间用来存储这个值,在定义新的变量名时,会重复指向这个内存空间(id)。

3.指向问题

这个比较简单,代码如下:

>>> a = 1
>>> b = a

执行a=1时,内存开辟并存储1这个整型,变量名a指向该内存空间;执行b=a时,定义新变量名b指向a所指向的内存,即1存储的空间
在这里插入图片描述

3.1指向问题+可变/不可变类型

如果可变/不可变类型问题比较理解的话,这个问题也比较简单,即看内存中保存的数据是否可变,如果可变,那么内存地址是允许你变化,仍然指向这个id,如果不可变,那么就需要另外开辟新的空间来存储这个值,首先以可变类型举例:

>>> l1 = [1,2]
>>> l2 = l1 # 指向
>>> id(l1)
140664892509000
>>> id(l2)
140664892509000
>>> l1.append(3)
>>> l1
[1, 2, 3]
>>> l2
[1, 2, 3]
>>> id(l1)
140664892509000
>>> id(l2)
140664892509000

在这里插入图片描述
可以看出来,b指向a,a指向[1,2],所以b也指向[1,2],当a变化时,b也会跟着变化,因为b指向的内存空间存储的值发生变化,同理,b变化,a也会跟着发生变化,列表,字典,集合是相同原理。
对于不可变类型:

>>> a = 1
>>> b = a
>>> id(a) == id(b)
True
>>> a += 1
>>> a
2
>>> b
1
>>> id(a) == id(b)
False

首先,a指向1所在地址,b指向a,即b也指向1的地址,此时a和b同时指向1所在地址,所以为True,但是1是不可变数据类型,即内存地址不允许1在原有的id上发生变化,所以1想要变成2,必须跳出原有id地址,另辟蹊径,开辟一个新的内存地址用来存储2,同时,a的指向也发生变化,a指向了2这个地址,但是b不变,b仍然指向1的地址
在这里插入图片描述
注:在python中 a = a+1 和 a += 1的结果是一样的,但是运行机制并不一样,有兴趣的老铁可以自行研究。

4.浅拷贝copy

浅拷贝涉及到copy模块的copy类,首先需要导入类,浅拷贝需要分情况讨论

  • 可变,例如列表
  • 不可变,例如整型
  • 外层不可变,元素中有可变,例如元组中元素是列表([1,2],[3,4])
  • 外层可变,元素中也有可变,例如列表嵌套列表[[1,2],[3,4]]

4.1浅拷贝可变数据

>>> l1 = [1,2]
>>> l2 = copy.copy(l1)
>>> id(l1) == id(l2)
False

当浅拷贝一个普通列表时,l2指向了一块新的内存区域,所以l1和l2是分离的,修改l1,l2也不会有所变化

4.2浅拷贝不可变数据

>>> t1 = (1,2)
>>> t2 = copy.copy(t1)
>>> id(t1) == id(t2)
True

当浅拷贝一个元组等不可变类型数据时,id没有发生变化,所以t1和t2指向了同一块内存。

4.3外层不可变,元素中有可变,例如元组中元素是列表([1,2],[3,4])

>>> a = [1,2]
>>> b = [3,4]
>>> c = (a,b) # ([1,2],[3,4])
>>> d = copy.copy(c)
>>> id(d) == id(c)
True
>>> a.append(5)
>>> c
([1, 2, 5], [3, 4])
>>> d
([1, 2, 5], [3, 4])

浅拷贝不可变数据元组,id确实不发生变化,但是为什么a修改之后d也发生了变化,因为浅拷贝是"浅显"的,浅拷贝没有完全的把内部可变数据"剥离"出来,即c中的a和b指向两个列表,浅拷贝的d也指向了相同的两个列表,所以a和b变化,c和d也会发生改变
在这里插入图片描述

4.4外层是可变数据类型,元素中也有可变,例如列表嵌套列表[[1,2],[3,4]]

>>> a = [1,2]
>>> b = [3,4]
>>> c = [a,b]
>>> d = copy.copy(c)
>>> c
[[1, 2], [3, 4]]
>>> d
[[1, 2], [3, 4]]
>>> id(c) == id(d)
False
>>> a.append(5)
>>> c
[[1, 2, 5], [3, 4]]
>>> d
[[1, 2, 5], [3, 4]]

浅拷贝可变数据,id会发生变化,似乎是重新定义了一个数据,但其实两者之间还有关联,正如上文所说,浅拷贝是"浅显"的,d中的两个列表仍然指向a和b。所以a变化时,c和d一起变。
在这里插入图片描述

4.5总结

浅拷贝不可变数据类型时,两个id不会发生变化,两个变量名指向相同;浅拷贝可变数据类型时,id一定会发生变化;基于上面两个基础,如果数据类型中有嵌套,而且嵌套的是可变类型数据,那么内部可变数据发生变化时,即使id不一样,c和d都会发生变化,即,浅拷贝只拷贝了最外层,内层仍然是指向。

5.深拷贝deepcopy

深拷贝涉及到copy模块的deepcopy类,首先需要导入类,深拷贝也需要分情况讨论

  • 可变,例如列表
  • 不可变,例如整型
  • 外层不可变,元素中有可变,例如元组中元素是列表([1,2],[3,4])
  • 外层可变,元素中也有可变,例如列表嵌套列表[[1,2],[3,4]]

5.1普通可变类型与浅拷贝一样,id发生变化。

>>> a = [1,2]
>>> b = copy.deepcopy(a)
>>> id(a) == id(b)
False

5.2普通不可变类型与浅拷贝一样,id不会发生变化。

>>> a = (1,2)
>>> b = copy.deepcopy(a)
>>> id(a) == id(b)
True

5.3外层不可变时,Attention,元素中有可变时与浅拷贝就有所区别了

>>> a = [1,2]
>>> b = [3,4]
>>> c = (a,b)
>>> d = copy.copy(c) # 浅拷贝
>>> id(c) == id(d) 
True
>>> e = copy.deepcopy(c) # 深拷贝
>>> id(e) == id(c) 
False
>>> a.append(5)
>>> c
([1, 2, 5], [3, 4]) 
>>> d
([1, 2, 5], [3, 4]) # 浅拷贝
>>> e
([1, 2], [3, 4]) # 深拷贝

在浅拷贝时,内部可变元素仍旧指向相同id位置,但是在深拷贝中,与原指向完全分离,可以理解为重新定义了一个元素,然后重新的指向新元素,所以id发生了变化。此时只有c中的a指向a,所以a变化之后,d不会跟着一起变。
在这里插入图片描述

5.4外层可变时,且内部元素也存在可变时,首先id肯定会变化(和浅拷贝一样),而且内部元素也会分离开来。

>>> a = [1,2]
>>> b = [3,4]
>>> c = [a,b]
>>> d = copy.deepcopy(c)
>>> id(c) == id(d)
False
>>> a.append(5)
>>> c
[[1, 2, 5], [3, 4]]
>>> d
[[1, 2], [3, 4]]

在这里插入图片描述

5.5总结

  1. 浅拷贝一个不可变类型时,id不会发生变化,但是在深拷贝中,如果不可变类型内元素都为不可变类型数据,那么深拷贝之后id也不会变化,但凡有一个可变数据类型,那么id就会变化,因为里面可变元素的指向发生了变化;
  2. 不管是深拷贝还是浅拷贝一个可变数据类型,id都会发生变化,但是深拷贝会把里面的可变数据"连根拔起"进行重新指向,浅拷贝只拷贝最外层。
  3. 小tip比较多,其实只要理解"指向"内存问题,就基本能理解浅拷贝与深拷贝,只需要记住浅拷贝和深拷贝都会对可变的数据进行重新指向就好了,只是浅拷贝重新指向的是最外层,深拷贝重新指向的是所有层。

6.其他

6.1列表的切片是浅拷贝

>>> a = [1,2]
>>> b = [3,4]
>>> c = [a,b]
>>> d = c[:]
>>> c
[[1, 2], [3, 4]]
>>> d
[[1, 2], [3, 4]]
>>> id(c)
139662719286920
>>> id(d)
139662719287048
>>> a.append(5)
>>> c
[[1, 2, 5], [3, 4]]
>>> d
[[1, 2, 5], [3, 4]]

6.2字典的copy方法是浅拷贝

>>> a = [1,2]
>>> d1 = {'a':a}
>>> d2 = d1.copy()
>>> id(d1)
139662719282704
>>> id(d2)
139662720918800
>>> a.append(3)
>>> d1
{'a': [1, 2, 3]}
>>> d2
{'a': [1, 2, 3]}

7.在写总结的"指向"问题时,我想起了导入模块的方式不同,也有不同的效果,感兴趣的老铁可以自行尝试

通过import和from…import…的方法都能导入我们所需要的模块中的方法或者类或者变量等等
import 模块
from 模块 import 类/方法/变量

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值