python的赋值及深拷贝和浅拷贝

1. 从编程中遇到的一个问题说起

我在变成的时候遇到一个问题让我很困惑,下面是我将那个例子简化后的样子:

import numpy as np

def process(b):
	b[1] = 10
	return b

a = np.array([1,2,3])
print(a)
c = process(a)
print(c)
print(a)

结果为:

[1 2 3]
[ 1 10  3]
[ 1 10  3]

这让我一度很困惑,因为之前编程的时候对上述程序的理解是其中a是一个实参,b是一个形参,在函数中的计算应该不会影响到实参的值,但这里却影响到了,这是怎么回事呢?

2. Python中的可变对象和不可变对象

经过查阅资料,回顾了一下之前学的Python的知识才发现问题出在Python的可变对象和不可变对象身上。
在Python中,按更新对象的方式,可以将对象分为2大类:可变对象与不可变对象。
可变对象: 列表、字典、集合。所谓可变是指可变对象在改变变量值的时候,它在内存空间中的ID是不变的。
不可变对象:数字、字符串、元组。不可变对象是指在改变变量值的时候,它在内存空间中的ID会发生变化,即新创建的对象被关联到原来的变量名,旧对象被丢弃,垃圾回收器会在适当的时机回收这些对象。

由此我们对第一部分提出的问题进行分析:

(1)当列表为普通列表的情况下

def process(b):
	print("ID_b=", id(b))
	b[1] = 5
	print("ID_b[1]=", id(b[1]))
	print("type_b[1]=", type(b[1]))
	b[1] = 10
	print("ID_b[1]=", id(b[1]))
	print("ID_b=", id(b))
	return b

def process1(b):
	print("ID_b=", id(b))
	b = b+1
	print("ID_b=", id(b))
	return b

a = [1,2,3]
print("a=", a)
print("ID_a=", id(a))
c = process(a)
print("ID_c=", id(c))
print("ID_a=", id(a))
print("c=", c)
print("a=", a)

结果为

a= [1, 2, 3]
ID_a= 2021304500360
ID_b= 2021304500360
ID_b[1]= 1796301088
type_b[1]= <class 'int'>
ID_b[1]= 1796301248
ID_b= 2021304500360
ID_c= 2021304500360
ID_a= 2021304500360
c= [1, 10, 3]
a= [1, 10, 3]

可以看到这样的结果符合前文对可变对象和不可变对象的描述,其中a是可变对象,因为它是一个列表,但是b[1]就是一个不可变对象,因为它是一个数字。刚开始给a赋值为[1,2,3],在进行函数调用的时候实参和形参的ID是相同的,故a和b都是可变对象,所以自始至终a和b的ID都没有变化,甚至是在返回值b赋给c的时候c的ID也跟a和b相同,所以当b的值在函数中改变的时候a和c的值也跟着改变了。但是b[1]的值改变前后b[1]的ID是变化的。

对函数进行一些改动

def process1(b):
	print("ID_b=", id(b))
	b.append(1)
	print("ID_b=", id(b))
	return b
a = [1,2,3]
print("a=", a)
print("ID_a=", id(a))
c = process1(a)
print("ID_c=", id(c))
print("ID_a=", id(a))
print("c=", c)
print("a=", a)

结果为

a= [1, 2, 3]
ID_a= 1979067400328
ID_b= 1979067400328
ID_b= 1979067400328
ID_c= 1979067400328
ID_a= 1979067400328
c= [1, 2, 3, 1]
a= [1, 2, 3, 1]

这与上面对b[1]改变形成了对比,因为b是可变对象,所以在对b进行改变的前后b的ID没有变化。

(2)当列表为numpy列表的情况下

def process(b):
	print("ID_b=", id(b))
	print("ID_b[1]=", id(b[1]))
	print("type_b[1]=", type(b[1]))
	b[1] = 10
	print("ID_b[1]=", id(b[1]))
	print("ID_b=", id(b))
	return b
a = np.array([1,2,3])
print("a=", a)
print("ID_a=", id(a))
c = process(a)
print("ID_c=", id(c))
print("ID_a=", id(a))
print("c=", c)
print("a=", a)

结果为

a= [1 2 3]
ID_a= 1454049343488
ID_b= 1454049343488
ID_b[1]= 1454062366200
type_b[1]= <class 'numpy.int32'>
ID_b[1]= 1454062366200
ID_b= 1454049343488
ID_c= 1454049343488
ID_a= 1454049343488
c= [ 1 10  3]
a= [ 1 10  3]

我们可以看到这里的b[1]变成了一个可变的对象,尽管它是一个数。

对函数进行一些改动

def process1(b):
	print("ID_b=", id(b))
	b = np.append(b,1)
	print("ID_b=", id(b))
	return b

a = np.array([1,2,3])
print("a=", a)
print("ID_a=", id(a))
c = process1(a)
print("ID_c=", id(c))
print("ID_a=", id(a))
print("c=", c)
print("a=", a)

结果为

a= [1 2 3]
ID_a= 2251005442048
ID_b= 2251005442048
ID_b= 2251010315888
ID_c= 2251010315888
ID_a= 2251005442048
c= [1 2 3 1]
a= [1 2 3]

可以看到这里的b变成了一个不可变的对象,尽管它是一个列表。

3. 解决问题

可以看到,因为2.中的情况比较复杂,所以在编程的时候会使程序混乱,所以需要解决这个问题,这样刚开始出现的问题也会迎刃而解。
这是就要用到深拷贝和浅拷贝了,先来说明一下什么叫深拷贝,什么叫浅拷贝以及什么叫赋值。

先用图片来直观地表示一下这三个概念:
在这里插入图片描述其中每一格子代表一个储存单元,可以看到赋值(b=a)仅仅是让b和a指向同一个ID,这样的话只要改变内容或子内容a和b的值都会变;而浅拷贝(b=copy(a))是将a所指向的第一层内容复制到了另一个存储单元并让b指向这个存储单元,但这两个单元共同指向同一个自内容所在的单元,这样的话只改变某一个内容的话另一个内容是不会变的,但改变自内容的话a和b都会变;而深拷贝(b=deepcopy(a))在浅拷贝的基础上将子内容复制到了另一个存储单元并让b的内容指向子内容,这样的话不管改变内容还是子内容,a和b都不会互相影响。

(1)赋值

list = [1,2,3]
print("ID_list=", id(list))
a = list
print("ID_a=", id(a))
list.append(5)
print("list=", list)
print("a=", a)
a.pop()
print("list=", list)
print("a=", a)

结果为

ID_list= 2139430031240
ID_a= 2139430031240
list= [1, 2, 3, 5]
a= [1, 2, 3, 5]
list= [1, 2, 3]
a= [1, 2, 3]

这就是一个普通的赋值过程,将list赋值给a,a就和list指向同一个ID,无论对其中哪个进行操作另一个也会同时被操作。

(2)浅拷贝

import copy
import numpy as np

def process(b):
	print("ID_b=", id(b))
	c = copy.copy(b)
	print("ID_c=", id(c))
	c[1] = 10
	print("ID_c=", id(c))
	return c

a = np.array([1,2,3])
print(a)
d = process(a)
print(d)
print(a)

结果为

[1 2 3]
ID_b= 1454656337920
ID_c= 1454656338160
ID_c= 1454656338160
[ 1 10  3]
[1 2 3]

可以看到,浅拷贝是将一个内存空间的内容复制给了另一个空间,所以在其中一个空间进行操作不会影响到另一个空间。浅拷贝可以用于解决刚开始的问题。另外程序中用浅拷贝比较多,因为拷贝速度快,占用空间少,拷贝效率高。

普通列表的情况:

iimport copy
import numpy as np

def process(b):
	print("ID_b=", id(b))
	c = copy.copy(b)
	print("ID_c=", id(c))
	print("ID_c[1]=", id(c[1]))
	c[1] = 10
	print("ID_c[1]=", id(c[1]))
	return c

a = [1,2,3]
print(a)
d = process(a)
print(d)
print(a)

结果

[1, 2, 3]
ID_b= 1940492649032
ID_c= 1940492565704
ID_c[1]= 1796300992
ID_c[1]= 1796301248
[1, 10, 3]
[1, 2, 3]

(3)深拷贝
讲到深拷贝就要和浅拷贝做一个对比才好理解

先来个浅拷贝的例子:

import copy
import numpy as np

def process(b):
	print("ID_b=", id(b))
	c = copy.copy(b)
	print("c=", c)
	a0[1] = 0
	print("ID_c=", id(c))
	print("ID_c[1]=", id(c[1]))
	c[1] = 10
	print("ID_c[1]=", id(c[1]))
	print("c=", c)
	return c

a0 = [4,5,6]
a = [1,2,3,a0]
print("a=", a)
d = process(a)
print("d=", d)
print("a=", a)

结果为

a= [1, 2, 3, [4, 5, 6]]
ID_b= 3262907788360
c= [1, 2, 3, [4, 5, 6]]
ID_c= 3262907875656
ID_c[1]= 1778475200
ID_c[1]= 1778475456
c= [1, 10, 3, [4, 0, 6]]
d= [1, 10, 3, [4, 0, 6]]
a= [1, 2, 3, [4, 0, 6]]

可以看到虽然在函数中只改变了a中的a0,但经过拷贝之后c的值也随着a0的变化发生了变了,这是因为浅拷贝仅能拷贝第一层的内容,也就是说只对a进行操作的时候是可以保持不变的,但对第二层a0进行操作的时候就会变化。

深拷贝的例子:

import copy
import numpy as np

def process(b):
	print("ID_b=", id(b))
	c = copy.deepcopy(b)
	print("c=", c)
	a0[1] = 0
	print("ID_c=", id(c))
	print("ID_c[1]=", id(c[1]))
	c[1] = 10
	print("ID_c[1]=", id(c[1]))
	print("c=", c)
	return c

a0 = [4,5,6]
a = [1,2,3,a0]
print("a=", a)
d = process(a)
print("d=", d)
print("a=", a)

结果为

a= [1, 2, 3, [4, 5, 6]]
ID_b= 2016855440520
c= [1, 2, 3, [4, 5, 6]]
ID_c= 2016855523720
ID_c[1]= 1778475200
ID_c[1]= 1778475456
c= [1, 10, 3, [4, 5, 6]]
d= [1, 10, 3, [4, 5, 6]]
a= [1, 2, 3, [4, 0, 6]]

可以看到深拷贝是在拷贝的时候拷贝了两层,即把a0也拷贝过去了,所以在a0发生变化的时候c不会变化。

参考

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

comli_cn

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值