Python copy模块浅拷贝和深拷贝

 2023-09-07 阅读 17 评论 0

摘要:Python copy模块浅拷贝和深拷贝 在开发中,经常涉及到数据的传递,在数据传递使用的过程中,可能会对数据进行修改。 对数据进行处理后,如果在后面的代码中,即需要使用修改之前的数据,也需要使用修改之后的数据,就要在修改前对

Python copy模块浅拷贝和深拷贝

在开发中,经常涉及到数据的传递,在数据传递使用的过程中,可能会对数据进行修改。

对数据进行处理后,如果在后面的代码中,即需要使用修改之前的数据,也需要使用修改之后的数据,就要在修改前对数据进行拷贝。

拷贝数据后,有两份数据,只修改其中一份数据,不修改另一份数据,到最后依然能保留修改前的数据。

一、Python 实现数据拷贝的方法

# coding=utf-8
import copybase = ['a', 'b', 'c', 'd', 'e']
# 切片
bak1 = base[:]
print("bak1: ", bak1)
# list工厂函数
bak2 = list(base)
print("bak2: ", bak2)
# python list对象的copy方法
bak3 = base.copy()
print("bak3: ", bak3)
# copy模块的copy方法
bak4 = copy.copy(base)
print("bak4: ", bak4)

运行结果:

bak1:  ['a', 'b', 'c', 'd', 'e']
bak2:  ['a', 'b', 'c', 'd', 'e']
bak3:  ['a', 'b', 'c', 'd', 'e']
bak4:  ['a', 'b', 'c', 'd', 'e']

上面的代码使用了四种方式来对数据进行拷贝,这些方法都可以用来拷贝数据,结果都一样。

1.切片

对需要拷贝的数据进行切片处理,返回的结果相当于拷贝了一份数据。

2.工厂方法

使用 python 的工厂函数 list 来拷贝数据。(python的工厂函数是比较特殊的,即是类也是函数,关于工厂函数的理解可以另行扩展一下)

拷贝列表时使用 list,如果拷贝字符串则将上面的 list 换成 str ,以此类推。

3.list对象的copy方法

python 中的 list 实现了 copy 方法,在拷贝列表时可以直接使用。这里需要注意,比如 str 没有实现 copy 方法,拷贝字符串时使用其他方法拷贝。

4.copy模块的copy方法

在 Python 标准库中有一个 copy 模块,可以使用 copy 模块的 copy() 方法来拷贝数据,copy 模块可以拷贝所有类型的数据。

二、拷贝的数据被修改

import copyson = ['python', 'copy']
base = ['a', 'b', 'c', 'd', 'e', son]
bak1 = base[:]
print("bak1: ", bak1)
bak2 = list(base)
print("bak2: ", bak2)
bak3 = base.copy()
print("bak3: ", bak3)
bak4 = copy.copy(base)
print("bak4: ", bak4)
print('-' * 20, '分割线', '-' * 20)
son[0] = 'PYTHON'
son[1] = 'COPY'
print('base: ', base)
print("bak1: ", bak1)
print("bak2: ", bak2)
print("bak3: ", bak3)
print("bak4: ", bak4)

运行结果:

bak1:  ['a', 'b', 'c', 'd', 'e', ['python', 'copy']]
bak2:  ['a', 'b', 'c', 'd', 'e', ['python', 'copy']]
bak3:  ['a', 'b', 'c', 'd', 'e', ['python', 'copy']]
bak4:  ['a', 'b', 'c', 'd', 'e', ['python', 'copy']]
-------------------- 分割线 --------------------
base:  ['a', 'b', 'c', 'd', 'e', ['PYTHON', 'COPY']]
bak1:  ['a', 'b', 'c', 'd', 'e', ['PYTHON', 'COPY']]
bak2:  ['a', 'b', 'c', 'd', 'e', ['PYTHON', 'COPY']]
bak3:  ['a', 'b', 'c', 'd', 'e', ['PYTHON', 'COPY']]
bak4:  ['a', 'b', 'c', 'd', 'e', ['PYTHON', 'COPY']]

在实际工作中,数据的嵌套层数是很多的,通常会嵌套好几层。上面就在 base 列表中嵌套了一个 son 子列表。

用上面的四种拷贝方法拷贝 base 列表,然后修改 base 列表中的子列表 son 。重新打印这几个列表,发现不仅 base 列表被修改了,拷贝的列表也全部被修改了。

现在的需求是拷贝一份数据,修改一份保留一份,如果两份数据都被修改,是不符合需求的。

上面的四种拷贝方法都被称为浅拷贝(相对深拷贝而言),浅拷贝 Python 中的可变对象,如果数据中嵌套了可变对象,修改嵌套的可变对象,所有拷贝的数据都会一起被修改。

三、Python 可变对象和不可变对象

在 Python 中,所有的数据都是对象,无论是数字,字符串,元组,列表,字典,还是函数,类,甚至是模块。

不可变对象:

int, str, tuple 等类型的数据是不可变对象,不可变对象的特性是数据不可被修改。

a = 'a'
print(id(a))
a = 'b'
print(id(a))

运行结果:

1543912659912
1543912658232

如果对不可变对象修改,其实不是修改变量对象,而是重新创建一个同名的变量对象。可以通过 id 函数来判断,id 不一样就证明已经不是同一个变量了。

可变对象:

list, set,dict 等类型的数据是可变对象,相对于不可变对象而言,可变对象的数据可以被修改,修改之后还是同一个id。

base = [1, 2, 3]
print(id(base))
base[0] = 100
print(base)
print(id(base))

运行结果:

2182371173000
[100, 2, 3]
2182371173000

对可变对象进行修改,修改后还是同一个对象,只是可变对象里面的元素指向了不同的数据,这种指向是通过引用的方式来实现的。

上面的代码是对列表进行修改,如果对元组这样修改,代码会报错,就是因为可变对象和不可变对象的区别。

四、Python 中的引用和引用传递

在 Python 程序中,每个对象都会在内存中开辟一块空间来保存该对象,该对象在内存中所在位置的地址被称为引用。

在编写代码时,定义的变量名实际是定义指向对象的地址引用名。

我们定义一个列表时,变量名是列表的名字,这个名字指向内存中的一块空间。这个列表里有多个元素,表示这块内存空间中,保存着多个元素的引用。

1. 修改引用

当修改列表的元素时,其实是修改列表中的引用。

list_a = [1, 2, 3]
list_a[2] = 30
print('list_a: ', list_a)

运行结果:

list_a:  [1, 2, 30]

例如,修改 list_a 中的第三个元素,其实是修改第三个元素的引用(这块内存指向的对象)。如下图:

2. 引用传递(拷贝)

当拷贝列表时,其实是拷贝列表中的引用。

list_b = [1, 2, 3]
list_c = list_b.copy()
print('list_c: ', list_c)

运行结果:

list_c:  [1, 2, 3]

例如,拷贝 list_b 到 list_c,其实是给 list_c 新开辟一块内存,然后拷贝一份 list_b 的引用给 list_c ,并不是将 list_b 指向的对象拷贝一份。如下图:

注意:这里不是将 list_b 赋值给 list_c,那样的结果是 list_b 指向 [1, 2, 3] ,list_c 指向 list_b,是引用关系,而不是拷贝关系。上面列举拷贝的方法时,没有将赋值列为拷贝方法,因为赋值是引用的传递,而不是拷贝。

五、浅拷贝时数据被修改

1. 拷贝后修改引用(数据无嵌套)

import copylist_b = [1, 2, 3]
list_c = copy.copy(list_b)
list_b[2] = 30
print('list_b: ', list_b)
print('list_c: ', list_c)

运行结果:

list_b:  [1, 2, 30]
list_c:  [1, 2, 3]

使用 copy.copy() 方法拷贝 list_b 到 list_c,然后修改 list_b 中的引用关系,这样, list_c 不会被修改。如下图:

2.嵌套列表的拷贝

import copysub = [2, 3]
list_d = [1, sub]
list_e = copy.copy(list_d)
print('list_d: ', list_d)
print('list_e: ', list_e)

运行结果:

list_d:  [1, [2, 3]]
list_e:  [1, [2, 3]]

对于嵌套的列表,拷贝 list_d 到 list_e,也是拷贝一份 list_d 的引用给 list_e ,与不嵌套的相同。

这里需要特别注意,在浅拷贝嵌套的列表时,只会拷贝最上层的引用,对于子列表的引用,不会拷贝。如下图:

3.拷贝的列表随原列表一起被修改

import copysub = [2, 3]
list_d = [1, sub]
list_e = copy.copy(list_d)
list_d[1][1] = 30
print('list_d: ', list_d)
print('list_e: ', list_e)

运行结果:

list_d:  [1, [2, 30]]
list_e:  [1, [2, 30]]

拷贝 list_d 到 list_e,由于没有拷贝子列表的引用 ,当修改子列时, list_d 和 list_e 都引用了子列表 sub,所以 list_d 和 list_e都会被修改。如下图:

拷贝数据后,修改其中一个,另一个也跟着被修改,原因就是浅拷贝中,只拷贝了最外层的引用。当修改内层的引用时,所有外层的引用不变,都会指向修改后的结果。

两份数据都被修改,这就是浅拷贝中存在的问题,需要使用深拷贝来解决。

六、深拷贝保证数据不会被修改

import copysub = [2, 3]
list_d = [1, sub]
list_f = copy.deepcopy(list_d)
list_d[1][1] = 30
print('list_d: ', list_d)
print('list_e: ', list_f)

运行结果:

list_d:  [1, [2, 30]]
list_e:  [1, [2, 3]]

使用 copy 模块的 deepcopy() 方法,在拷贝数据时,会递归地拷贝数据中所有嵌套的引用。

使用 deepcopy() 拷贝 list_d 到 list_f ,然后修改 list_d 中子列表的引用,不会对 list_f 产生影响,所以 list_f 不会被修改。如下图:(可以和上面的图进行对比)

这样,就可以达到复制数据,一份修改,一份不修改的目的。

在工作中,这种情况不是特别多,所以出现的时候很容易掉坑。比如说,有一个复杂的列表(字典、列表多层嵌套),第一次获取数据写入数据库,后面每次获取数据都要与上一次的数据对比去重,然后把本次获取的数据覆盖数据库中的数据。这就是获取数据,修改数据,最后还需要使用修改前的数据。这时候用浅拷贝,极易出错,对于较大的数据(如一个1M大的json数据),出错了还不易发现错误。

为了解决和避免这种错误,可以使用深拷贝 deepcopy()。

在Python中,浅拷贝消耗的内存和运行效率都优于深拷贝,所以默认的拷贝都是浅拷贝。

对可能需要使用深拷贝的情况,要特别留意,使用深拷贝,避免出错。

 

 

 

版权声明:本站所有资料均为网友推荐收集整理而来,仅供学习和研究交流使用。

原文链接:https://hbdhgg.com/3/10338.html

发表评论:

本站为非赢利网站,部分文章来源或改编自互联网及其他公众平台,主要目的在于分享信息,版权归原作者所有,内容仅供读者参考,如有侵权请联系我们删除!

Copyright © 2022 匯編語言學習筆記 Inc. 保留所有权利。

底部版权信息