python中array赋值_Python中的赋值与深浅拷贝

9068e539a9f2eb1dd9a242178f7fbb4d.gif

​在 Python 中,很多人常常搞不清楚赋值操作与深浅拷贝的区别,在实际使用是稍不注意可能产生意想不到的结果。

本篇文章我们就来看一下 Python 中赋值与深浅拷贝的区别。

1 赋值

我们先来看一个示例:

a = 1
b = a
print(id(a))
print(id(b))
a = a + 1
print(id(a))
print(id(b))
# 输出
140704045741856
140704045741856
140704045741888
140704045741856

首先给变量 a 赋值 1,即 a 指向 1 这个对象。然后将 a 赋值给 b,b 也指向 1 这个对象(a 和 b 指向的内存地址相同)。最后执行 a = a + 1,a 的值变成了 2,指向了 2 这个对象,2 是新创建的一个对象。但是 b 并不随 a 变化而变化,仍然指向 1。

9b47f10c90899fc163f52e87384ca4f3.png

从这个例子可以看到,a 和 b 开始只是指向对象的变量而已,赋值操作其实就是对象的引用,相当于给对象起了两个别名。同一个对象可以被多个变量指向或引用。

由于 a 和 b 指向的 1 是不可变对象,尽管执行了 b = a,a 和 b 并没绑定在一起,当给 a 重新赋值时,b 并不随 a 的改变而改变。

我们继续再看一个示例:

a = [1, 2, 3, 4]
b = a
print(f'a={a}, b={b}')
print(f'id(a)={id(a)}, id(b)={id(b)}')
# 输出
a=[1, 2, 3, 4], b=[1, 2, 3, 4]
id(a)=2427363725376, id(b)=2427363725376

首先 a 指向列表这个可变对象,然后再将 a 赋值给 b,从输出结果值及对应的内存地址可以看到它们指向的对象相同。

8215b933e9e7558915e8f94f321f83c8.png
a[1] = 'a'
print(f'a={a}, b={b}')
print(f'id(a)={id(a)}, id(b)={id(b)}')
# 输出
a=[1, 'a', 3, 4], b=[1, 'a', 3, 4]
id(a)=2427363725376, id(b)=2427363725376

然后修改 a 指向的列表元素值,由于列表是可变的,修改列表元素值不会改变原列表的内存地址。从输出结果也可以看出 a 和 b 指向的内存地址不变,只是存储在该地址上的列表对象发生变化。

a = ['a', 'b' , 'c', 'd']
print(f'id(a)={id(a)}, id(b)={id(b)}')
# 输出
id(a)=2427365031552, id(b)=2427363725376

如果我们给 a 赋值一个新列表,即创建一个新的对象,会开辟一个新的地址空间,内存地址发生变化。而 b 并不会随 a 的变化而变化,仍然指向原来的对象。

最后我们再来看一种情况:将值相同的对象赋值给变量。

s1 = 'python'
s2 = 'python'
print(f'id(s1)={id(s1)},id(s2)={id(s2)}')
l1 = [1, 2, 3]
l2 = [1, 2, 3]
print(f'id(l1)={id(l1)},id(l2)={id(l2)}')
# 输出
id(s1)=1973170558128,id(s2)=1973170558128
id(l1)=1973169229888,id(l2)=1973170536064

从以上结果可以看出,将值相同的不可变对象赋值给变量,两个变量指向的地址相同。而将值相同的不可变对象赋值给变量,两个变量指向的地址不同。结果表明,尽管列表值是相同的,但它们并不是同一个对象,而是开辟了两个地址空间。

小结:

1. 赋值操作其实就是对象的引用,多个变量可以指向同一个对象。

2. 将一个变量赋值给另一个变量,它们指向同一个对象。对于不可变对象,所有指向该对象的值不会改变,总是一样的。对于可变对象,如果改变对象的值,则指向该对象的变量都会受影响。如果给一个变量重新赋值新的对象,其他变量不受影响,仍指向原对象。

3. 值相同的不可变对象即使赋值给不同的变量,仍然是同一个对象,指向同一个地址。而可变对象则表示创建了多个值相同的不同对象,变量指向的地址也不同。

2 浅拷贝

我们先不直接给出什么是浅拷贝,先来看一个示例:

import copy

n = 1
s = 'python'
b = bool
n1 = copy.copy(n)
s1 = copy.copy(s)
b1 = copy.copy(b)
print(f'id(n)={id(n)},id(n1)={id(n1)}')
print(f'id(s)={id(s)},id(s1)={id(s1)}')
print(f'id(b)={id(b)},id(b1)={id(b1)}')
# 输出
id(n)=140732597024544,id(n1)=140732597024544
id(s)=2591688840368,id(s1)=2591688840368
id(b)=140732596742320,id(b1)=140732596742320

上述示例中,使用 copy 模块中的 copy() 方法实现浅拷贝操作。对数字、字符串、布尔值这些不可变对象进行浅拷贝,发现拷贝前后地址不变,也就是说拷贝前后的变量都还是指向同一个对象,相当于给对象又起了别名。

那我们可能会继续想,对于可变对象的浅拷贝操作结果是什么呢?下面我们继续看一个示例:

import copy

lst = [1, 2, 'python']
dic = {'name':'lin', 'age':27}
lst1 = list(lst)
dic1 = copy.copy(dic)
print(f'id(lst)={id(lst)},id(lst1)={id(lst1)}') # 拷贝前后列表地址
print(f'id(dic)={id(dic)},id(dic1)={id(dic1)}') # 拷贝前后字典地址
print(f'id(lst[2])={id(lst[2])},id(lst1[2])={id(lst1[2])}') # 拷贝前后列表元素地址
print(f'id(dic["name"])={id(dic["name"])},id(dic1["name"])={id(dic1["name"])}') # 拷贝前后字典值地址
# 输出
id(lst)=2224415633792,id(lst1)=2224415631744
id(dic)=2224414153408,id(dic1)=2224414153472
id(lst[2])=2224415616048,id(lst1[2])=2224415616048
id(dic["name"])=2224415609584,id(dic1["name"])=2224415609584

上面这个示例中我们分别对列表和字典执行浅拷贝操作。这里使用列表本身的构造器 list() 来实现浅拷贝操作, lst1 就是 lst 的浅拷贝。列表 lst 内的元素都是不可变数据类型。

从上面的例子可以看出:

1. 无论是对列表还是字典,拷贝前后的变量不再指向同一个对象,地址是不同的。

2. 拷贝前后列表中元素的地址是相同的,字典中的值地址也是相同的。

我们可以通过一个关系示意图来说明这个关系:

2b39b1d3cde0d42cf41c4b171e6955da.png

由于字典的键值对始终是不可变数据类型,所以在执行浅拷贝操作前后,对于键或值的地址是相同的。

对于列表,可能存在嵌套列表的情况,此时执行浅拷贝操作结果又是如何,我们同样通过示例来看:

lst = [1, 2, [3, 4, 5]]
lst1 = lst[:]
print(f'id(lst)={id(lst)},id(lst1)={id(lst1)}') # 拷贝前后列表地址
print(f'id(lst[2])={id(lst[2])},id(lst1[2])={id(lst1[2])}') # 拷贝前后列表中子列表地址
print(f'id(lst[2][0])={id(lst[2][0])},id(lst1[2][0])={id(lst1[2][0])}') # 拷贝前后列表中子列表元素地址
# 输出
id(lst)=1771712921280,id(lst1)=1771713061312
id(lst[2])=1771711631424,id(lst1[2])=1771711631424
id(lst[2][0])=140732099737440,id(lst1[2][0])=140732099737440

上面这个示例中列表中嵌套了一个子列表,然后进行浅拷贝操作。这里我们通过切片操作符 : 完成浅拷贝。从执行结果可以看到嵌套的子列表和子列表中的元素在拷贝前后地址不变。

我们再看一个示例来进一步理解浅拷贝操作:

lst = [1, 2, [3, 4, 5]]
lst1 = lst[:]
lst[2].append(6)
lst.append(7)
print(f'lst={lst}')
print(f'lst1={lst1}')
# 输出
lst=[1, 2, [3, 4, 5, 6], 7]
lst1=[1, 2, [3, 4, 5, 6]]

首先给子列表添加元素,发现浅拷贝前后列表中的子列表都发生变化。由于列表是可变对象,所以子列表只是值改变,所在的内存地址不变。拷贝前后列表中元素仍然指向相同的对象。然后我们给原列表添加元素,发现拷贝后的对象并不随原列表的变化而变化,因为 lst 和 lst1 作为整体是两个不同的对象,不共享内存地址。

小结:

1. 对于不可变对象,执行浅拷贝操作后,仍然指向相同的对象,相当于给对象又起了别名。

2. 对于可变对象,浅拷贝只是复制了最外层的数据,创建了一个新的对象,内部元素仍然是原对象中子对象的引用,指向相同的地址。

3 深拷贝

不同于浅拷贝的是,深拷贝会拷贝所有的可变数据类型,包括嵌套的可变数据。

同样地,我们先看一下不可变对象地深拷贝示例:

import copy

s = 'python'
s1 = copy.deepcopy(s)
print(f'id(s)={id(s)},id(s1)={id(s1)}')
# 输出
id(s)=2290851599856,id(s1)=2290851599856

从结果可以看出,不可变数据类型地深拷贝,指向相同的地址。这里,使用 copy 模块中的 deepcopy() 方法实现深拷贝

我们接着看一下可变对象的深拷贝:

import copy

lst = [1, 2, 'python']
dic = {'name':'lin', 'age':27}
lst1 = copy.deepcopy(lst)
dic1 = copy.deepcopy(dic)
print(f'id(lst)={id(lst)},id(lst1)={id(lst1)}') # 拷贝前后列表地址
print(f'id(dic)={id(dic)},id(dic1)={id(dic1)}') # 拷贝前后字典地址
print(f'id(lst[2])={id(lst[2])},id(lst1[2])={id(lst1[2])}') # 拷贝前后列表元素地址
print(f'id(dic["name"])={id(dic["name"])},id(dic1["name"])={id(dic1["name"])}') # 拷贝前后字典值地址
# 输出
id(lst)=1675074531776,id(lst1)=1675074529792
id(dic)=1675068791488,id(dic1)=1675074528384
id(lst[2])=1675074514032,id(lst1[2])=1675074514032
id(dic["name"])=1675074507568,id(dic1["name"])=1675074507568

从结果可以看出:如果可变对象中的元素都是不可变的,深拷贝前后两个变量指向的内存地址不再相同,说明创建了一个新的对象。但是再看内部元素的地址,拷贝前后地址相同,与浅拷贝是相同的效果。

如果可变对象内部也存在可变子对象,深拷贝的结果又是如何?

import copy

lst = [1, 2, [3, 4, 5]]
lst1 = copy.deepcopy(lst)
print(f'id(lst)={id(lst)},id(lst1)={id(lst1)}') # 拷贝前后列表地址
print(f'id(lst[2])={id(lst[2])},id(lst1[2])={id(lst1[2])}') # 拷贝前后列表中子列表地址
print(f'id(lst[2][0])={id(lst[2][0])},id(lst1[2][0])={id(lst1[2][0])}') # 拷贝前后列表中子列表元素地址
# 输出
id(lst)=2721310434240,id(lst1)=2721311559424
id(lst[2])=2721310437120,id(lst1[2])=2721311558720
id(lst[2][0])=140725548562272,id(lst1[2][0])=140725548562272

从结果可以看到深拷贝前后可变子对象的地址不同,而可变子对象的元素地址是相同,因为它们都是不可变数字类型,不受深拷贝的影响。

我们也可以通过一个关系示意图来说明深拷贝的关系:

f2cd8e64315ca0232cca2377e36c3d87.png

如果修改原对象的可变子对象,拷贝对象是否也随之改变?

import copy

lst = [1, 2, [3, 4, 5]]
lst1 = copy.deepcopy(lst)
lst[2].append(6)
lst.append(7)
print(f'lst={lst},lst1={lst1}')
# 输出
lst=[1, 2, [3, 4, 5, 6], 7],lst1=[1, 2, [3, 4, 5]]

从结果可以看到深拷贝前后的对象是相互独立的,原对象的父对象和子对象改变都不影响拷贝后的对象。

小结:

1. 不可变对象的深浅拷贝效果相同。

2. 对于可变对象的深拷贝,如果嵌套了可变子对象,会同时复制父对象和嵌套的子对象,并且拷贝后的对象和原对象互不影响。

前面我们已经通过示例详细介绍了深浅拷贝的特性,并且得出一个结论:对于不可变对象的深浅拷贝都指向相同的地址,这一定完全成立吗?我们来看一种特例。

元组是一个不可变数据类型,但内部可以嵌套可变的数据类型,那这样的元组深浅拷贝如何呢?

import copy

lst = (1, 2, [3, 4, 5])
lst1 = copy.copy(lst)
lst2 = copy.deepcopy(lst)
print(f'id(lst)={id(lst)}')
print(f'id(lst1)={id(lst1)}')
print(f'id(lst2)={id(lst2)}')
# 输出
id(lst)=2207173551488
id(lst1)=2207173551488
id(lst2)=2207175150144

我们可以看到对于浅拷贝我们之前的结论是成立的,但对于深拷贝来说,拷贝前后的内存地址是不同,这里要注意!

—END—

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值