【Python基础】Python的深浅拷贝讲解

文章参考来源于Datawhale ,作者皮钱超

目录

    • 前言
    • 一、Python对象
    • 二、数据类型
      • 2.1 可变和不可变类型
      • 2.2 引用
    • 三、赋值
      • 3.1 相同数据,不同变量名
      • 3.2 一个变量多次赋值
      • 3.3 变量赋值给变量
      • 3.4 嵌套赋值
    • 四、浅拷贝
      • 4.1 不可变类型的浅拷贝
      • 4.2 可变类型的浅拷贝
    • 五、深拷贝
      • 5.1 不可变类型的深拷贝
      • 5.2 可变类型的深拷贝
    • 六、元组的深浅拷贝
      • 6.1 不存在嵌套结构
      • 6.2 存在嵌套结构
    • 七、is和==
    • 八、总结

前言

在很多语言中都存在深浅拷贝两种拷贝数据的方式,Python中也不例外。本文中详细介绍了Python中的深浅拷贝的相关知识,文章的内容包含:

  • 对象、数据类型、引用
  • 赋值
  • 浅拷贝
  • 深拷贝
    【Python基础】Python的深浅拷贝讲解_第1张图片

一、Python对象

我们经常听到:Python中一切皆为对象。其实,说的就是我们在Python中构造的任何数据类型都是一个对象,不管是数字、字符串、字典等常见的数据结构,还是函数,甚至是我们导入的模块等,Python都会把它当做是一个对象来处理。
所有的Python对象都拥有3个属性:

  • 身份
  • 类型

我们看一个简单的例子来理解上面的3个属性:
假设我们声明了一个name变量,通过id、type方法能够查看对象的身份和类型:
【Python基础】Python的深浅拷贝讲解_第2张图片
甚至是type本身也是一个对象,它也拥有自己的身份、类型:
【Python基础】Python的深浅拷贝讲解_第3张图片

二、数据类型

2.1 可变和不可变类型

在Python中,按照更新对象的方式,我们可以将对象分为2大类:可变数据类型不可变数据类型

  • 不可变数据类型:数值字符串布尔值。不可变对象就是对象的身份和值都不可变。新创建的对象被关联到原来的变量名,旧对象被丢弃,垃圾回收器会在适当的时机回收这些对象。
  • 可变数据类型:列表字典集合。所谓的可变指的是可变对象的值可变,但是身份是不可变的。

首先我们看看不可变对象:
【Python基础】Python的深浅拷贝讲解_第4张图片
当我们定义了一个对象str1,给其赋值了“python”,便会在内存中找到一个固定的内存地址来存放;但是,当我们将“python”定义成另一个变量名的时候,我们发现:它在内存中的位置是不变的
【Python基础】Python的深浅拷贝讲解_第5张图片
也就是说,这个变量在计算机内存中的位置是不变的,只是换了一个名字来存放,来看3个实际的例子:

# 不可变对象
# 1.字符串
# 将字符串'python'赋值给str1
str1 = 'python'
# 将字符串'python'赋值给str2
str2 = 'python'
# 打印对象地址,发现内存地址不变
print(id(str1))  # 2699006485368
print(id(str2))  # 2699006485368

# 2.数值型
number1 = 5
number2 = 5
print(id(number1))  # 1578991856
print(id(number2))  # 1578991856

# 3.布尔类型
bool1 = True
bool2 = True
print(id(bool1))  # 1578486192
print(id(bool2))  # 1578486192

以上的例子说明:当我们对字符串数值型布尔值的数据改变变量名时,并不会影响到数据在内存中的位置。
我们看看可变类型的例子:

# 可变数据类型
# 1.列表
# 将列表[1, 2, 3]赋值给list1
list1 = [1, 2, 3]
# 将相同列表[1, 2, 3]赋值给list2
list2 = [1, 2, 3]
# 打印他们的内存地址,发现list1和list2指向的地址不同
print(id(list1))  # 2156011316872
print(id(list2))  # 2156010478216

# 2.字典
dict1 = {'name': 'zhangsan', 'age': 23}
dict2 = {'name': 'zhangsan', 'age': 23}
# 打印他们的内存地址,发现dict1和dict2指向的地址不同
print(id(dict1))  # 2012353158672
print(id(dict2))  # 2012353158744

# 3.集合
set1 = {1, 2, 3}
set2 = {1, 2, 3}
# 打印他们的内存地址,发现set1和set2指向的地址不同
print(id(set1))  # 1880191426344
print(id(set2))  # 1880190894824

于是,对于可变类型:列表字典集合,虽然是相同的数据,但是变量名字不同,内存中仍然会开辟新的内存地址来进行存放相同的数据,我们以字典为例:
【Python基础】Python的深浅拷贝讲解_第6张图片

2.2 引用

在Python语言中,每个对象都会在内存中申请开辟一块新的空间来保存对象;对象在内存中所在位置的地址称之为引用。

可以说,我们定义的变量名实际上就是对象的地址引用。引用实际上就是内存中的一个数字地址编号。在使用对象的时候,只要知道这个对象的地址,我们就可以操作这个对象。

因为这个数字地址不太容易记忆,所以我们使用变量名的形式来代替对象的数字地址。在Python中,变量就是地址的一种表示形式,并不会开辟新的存储空间。
我们通过一个例子来说明变量和变量指向的引用(内存地址)实际上就是一个东西:
【Python基础】Python的深浅拷贝讲解_第7张图片

三、赋值

3.1 相同数据,不同变量名

前面讲了:对于不可变数据类型字符串、数值、布尔,将他们赋值给不同的变量时,他们在内存中的地址不会发生改变;对于可变数据类型列表、字典、集合,将相同数据赋值给不同变量时,内存会开辟新的地址来存储。
另外,对可变数据类型进行操作,如添加、修改、删除时,不会改变他在内存中的地址:

list3 = [3, 4, 5]
print(id(list3))  # 2659645120648

list3.append(6)
print(id(list3))  # 2659645120648

list3.remove(3)
print(id(list3))  # 2659645120648

list3[0] = 9
print(id(list3))  # 2659645120648

3.2 一个变量多次赋值

如果我们对同一个变量多次赋值,其内存地址是会变化的:

# 如果我们对一个变量多次赋值,其内存地址是会变化的:
# 字符串
str3 = 'python'
print(id(str3))  # 2141134693240

str3 = 'learn python'
print(id(str3))  # 2141164761840
str3 = 'python'
print(id(str3))  # 2847122301816

# 列表
list4 = [1, 2, 3]  # 2090732414536
print(id(list4))

list4 = [1, 2, 3]  # 2090732413960
print(id(list4))

3.3 变量赋值给变量

将一个变量赋值给另一个变量,其实它们就是同一个对象:数据相同,在内存中的地址也相同:

a = 3
b = a
print(a == b)  # True 结果为True,表名数值相同
print(a is b)  # True 结果为True,表名对象相同
print(id(a) == id(b))  # True 结果为True,表名内存地址相同

x = [2, 3, 4]
y = x
print(x == y)  # True 结果为True,表名数值相同
print(x is y)  # True 结果为True,表名对象相同
print(id(x) == id(y))  # True 结果为True,表名内存地址相同

实际上它们就是同一个对象

3.4 嵌套赋值

如果是列表中嵌套着另外的列表,那么当改变其中一个列表的时候,另一个列表中的也会随着改变:

d = [2, 4, 5]
dd = [1, 2, d] # dd嵌套d
d.append(0)
print(d)  # [2, 4, 5, 0]
print(dd)  # [1, 2, [2, 4, 5, 0]] # d中追加元素0,dd中也追加

总结:赋值其实就是将一个对象的地址赋值给一个变量,使得变量指向该内存地址

四、浅拷贝

4.1 不可变类型的浅拷贝

如果只是针对不可变的数据类型(字符串、数值型、布尔值),浅拷贝的对象和原数据对象是相同的内存地址:

import copy

# 不可变类型的浅拷贝
str4 = 'welcome'
print(id(str4))  # 原数据地址 1953500315184
str5 = copy.copy(str4)  # 浅拷贝后的地址 1953500315184
print(id(str5))

c = 1
print(id(c))  # 原数据地址 1578991728
d = copy.copy(c)
print(id(d))  # 浅拷贝后的地址 1578991728

bool3 = False
print(id(bool3))  # 原数据地址 1578486224
bool4 = copy.copy(bool3)
print(id(bool4))  # 浅拷贝后的地址 1578486224

由此我们可以看出来:针对不可变类型的浅拷贝,只是换了一个变量名字,对象在内存中的地址其实是不变的

4.2 可变类型的浅拷贝

首先我们讨论的是不存在嵌套类型的可变类型数据(列表、字典、集合):

import copy

# 可变对象的浅拷贝
list5 = [3, 4, 5]
print(id(list5))  # 原数据地址 2451410125832
list6 = copy.copy(list5)
print(id(list6))  # 浅拷贝后的地址 2451410125768

print(id(list5[0]))  # 1578991792 列表中的数值的内存地址不变
print(id(list6[0]))  # 1578991792
print(id(list5[1]))  # 1578991824
print(id(list6[1]))  # 1578991824
print('++++++++++')
dict3 = {3: 2, 4: 1, 5: 2}
print(id(dict3))  # 原数据地址 2451410125832
dict4 = copy.copy(dict3)
print(id(dict4))  # 浅拷贝后的地址 2451410125768

set3 = {3, 4, 5}
print(id(set3))  # 原数据地址 1771435842024
set4 = copy.copy(set3)
print(id(set4))  # 浅拷贝后的地址 1771435841800

从上面的例子看出来:

  • 列表本身的浅拷贝对象的地址和原对象的地址是不同的,因为列表是可变数据类型。
  • 列表中的元素(第1个元素为例)和浅拷贝对象中的第一个元素的地址是相同的,因为元素本身是数值型,是不可变的。
  • 字典中也存在相同的情况:字典本身的内存地址不同,但是里面的键、值的内存地址是相同的,因为键值都是不可变类型的数据。

【Python基础】Python的深浅拷贝讲解_第8张图片

如果可变类型的数据中存在嵌套的结构:

list7 = [1, 2, 3, [4, 3, 2, 1]]
# 打印list7的内存地址
print(id(list7))  # 原数据地址 1786030986120
list8 = copy.copy(list7)
# 打印list8的内存地址
print(id(list8))  # 浅拷贝后的地址 1786030986120

# 打印外层列表第一个元素的地址
print(id(list7[0]))  # 1578991728
print(id(list8[0]))  # 1578991728

# 打印list7中嵌套列表的内存地址
print(id(list7[3]))  # 原嵌套列表的内存地址 1591596684296
print(id(list8[3]))  # 浅拷贝嵌套列表的内存地址 1591596684296
 
# 打印嵌套列表第一个元素的地址
print(id(list7[3][0]))  # 1578991824
print(id(list8[3][0]))  # 1578991824

由此我们可以看出,

  • 在可变类型的数据中,如果存在嵌套的结构类型,浅拷贝只复制最外层的数据,导致内存地址发生变化,里面数据的内存地址不会变
  • 浅拷贝只是拷贝数据的第一层,不会拷贝子对象。

五、深拷贝

深拷贝不同于浅拷贝的是:深拷贝会拷贝所有的可变数据类型,包含嵌套的数据中的可变数据。深拷贝是把变量对应的值复制到新的内存地址中,而不是复制数据对应的内存地址。

5.1 不可变类型的深拷贝

对于不可变类型的深拷贝,其效果和浅拷贝相同:

# 不可变类型的深拷贝
# 字符串、数值型、布尔型均是相同效果
x1 = 'uzi'
x2 = copy.deepcopy(x1)
x3 = copy.copy(x1)
print(id(x1))  # 原数据地址 2679680565688
print(id(x2))  # 深拷贝地址 2679680565688
print(id(x3))  # 浅拷贝地址 2679680565688

由此我们知道,对于不可变数据类型,深浅拷贝效果相同,只是变量名发生改变,其指向的内存地址不变

5.2 可变类型的深拷贝

首先我们讨论的是不存在嵌套的情况:

# 可变类型的深拷贝
# 无嵌套
# 列表
list0 = [1, 2, 4]
list01 = copy.deepcopy(list0)
list02 = copy.copy(list0)

print(id(list0))  # 原数据地址 1333752300424
print(id(list01))  # 深拷贝地址 1333752300360
print(id(list02))  # 浅拷贝地址 1333752300296

print(id(list0[0]))  # 原数据地址 1578991728
print(id(list01[0]))  # 深拷贝地址 1578991728
print(id(list02[0]))  # 浅拷贝地址 1578991728

# 字典
dict0 = {'name': 'uzi', 'age': 18}
dict01 = copy.deepcopy(dict0)
dict02 = copy.copy(dict0)

print(id(dict0))  # 原数据地址 1333752282064
print(id(dict01))  # 深拷贝地址 1333752282424
print(id(dict02))  # 浅拷贝地址 1333752282640

print(id(dict0['name']))  # 原数据地址 1333752529336
print(id(dict01['name']))  # 深拷贝地址 1333752529336
print(id(dict02['name']))  # 浅拷贝地址 1333752529336

我们可以得出结论:

  • 深拷贝对最外层数据是只拷贝数据,会开辟新的内存地址来存放数据
  • 深拷贝对里面的不可变数据类型直接复制数据和地址,和可变类型的浅拷贝是相同的效果
    【Python基础】Python的深浅拷贝讲解_第9张图片
    我们继续讨论存在嵌套类型的深拷贝(以列表为例):
# 可变类型的深拷贝
# 嵌套
list9 = [1, 2, 3, [3, 2, 1]]
list91 = copy.deepcopy(list9)
list92 = copy.copy(list9)

print(id(list9))  # 原数据地址 2808602888072
print(id(list91))  # 深拷贝地址 2808602724808
print(id(list92))  # 浅拷贝地址 2808602897736

print(id(list9[0]))  # 原数据地址 1578991728
print(id(list91[0]))  # 深拷贝地址 1578991728
print(id(list92[0]))  # 浅拷贝地址 1578991728

print(id(list9[3]))  # 原数据地址 2808602886600
print(id(list91[3]))  # 深拷贝地址 2808602731144
print(id(list92[3]))  # 浅拷贝地址 2808602886600

print(id(list9[3][0]))  # 原数据地址 1578991792
print(id(list91[3][0]))  # 深拷贝地址 1578991792
print(id(list92[3][0]))  # 浅拷贝地址 1578991792

我们得出结论:

  • 对整个存在嵌套类型的数据进行深浅拷贝都会发生内存的变化,因为数据本身是可变的。
  • 我们查看外层列表的第一个元素的内存地址,发现原地址、深拷贝地址和浅拷贝地址三者是相同的,因为元素是属于数值型,是不可变类型
  • 我们查看第四个元素即里面嵌套列表的内存地址,发现只有深拷贝是不同的,因为这个嵌套的列表是可变数据类型,深拷贝在拷贝了最外层之后还会继续拷贝子层级的可变类型。
  • 我们查看嵌套列表中的元素的内存地址,发现它们是相同的,因为元素是数值型,是不可变的,不受拷贝的影响。

六、元组的深浅拷贝

元组本身是不可变数据类型,但是其中的值是可以改变的,内部可以有嵌套可变数据类型,比如列表等,会对它的拷贝结果造成影响。

6.1 不存在嵌套结构

当元组中不存在嵌套结构的时候,元组的深浅拷贝是相同的效果:

# 元组的深浅拷贝
# 无嵌套
tuple0 = (1, 2, 3)
tuple1 = copy.deepcopy(tuple0)
tuple2 = copy.copy(tuple0)

print(id(tuple0))  # 原数据地址 1907138307272
print(id(tuple1))  # 深拷贝地址 1907138307272
print(id(tuple2))  # 浅拷贝地址 1907138307272

即,对于元组深浅拷贝只是将数值赋值给了新变量,其指向的内存地址不变

6.2 存在嵌套结构

当元组的数据中存在嵌套的可变类型,比如列表等,深拷贝会重新开辟地址,将元组重新生成一份。

# 元组的深浅拷贝
# 存在嵌套
tuple0 = (1, 2, 3, [3, 2, 1])
tuple1 = copy.deepcopy(tuple0)
tuple2 = copy.copy(tuple0)

print(id(tuple0))  # 原数据地址 2696108714968
print(id(tuple1))  # 深拷贝地址 2696078194632
print(id(tuple2))  # 浅拷贝地址 2696108714968

print(id(tuple0[3][0]))  # 原数据地址 1578991792
print(id(tuple1[3][0]))  # 深拷贝地址 1578991792
print(id(tuple2[3][0]))  # 浅拷贝地址 1578991792

七、is和==

在文章的开始就已经谈过:在Python中每个变量都有自己的标识、类型和值。每个对象一旦创建,它的标识就绝对不会变。一个对象的标识,我们可以理解成其在内存中的地址。is()运算符比较的是两个对象的标识;id()方法返回的就是对象标识的整数表示。

总结:is()比较对象的标识;==运算符比较两个对象的值(对象中保存的数据)。在实际的编程中,我们更多关注的是值,而不是标识本身。

# is 和 ==
v1 = [1, 2, 3, 4]
v2 = [1, 2, 3, 4]
print(v2 == v1)  # True
print(v2 is v1)  # False

v3 = 'uzi'
v4 = 'uzi'
print(v3 == v4)  # True
print(v3 is v4)  # True

八、总结

  • 对于不可变数据类型,深拷贝和浅拷贝会将对象的数值赋值给新变量,内存地址不变,不会在内存中开辟新空间。
  • 对于可变数据类型:如果不存在嵌套,深浅拷贝都会开辟一个新的内存空间,同时其中的数值即不可变类型仍然指向原来的内存地址;如果存在嵌套,浅拷贝只会拷贝最外层的数据,而深拷贝会拷贝所有层级的子对象和可变类型数据。

你可能感兴趣的:(Python,python,笔记)