Python世界:复制粘贴?没那么简单!浅谈深拷贝与浅拷贝
问题引入
Python实现中,最近遇到个小问题,对其中的拷贝理解更深了些,这里小结下。
假设我们都知道以下常识:
- 浅拷贝,值引用;只创建了新对象地址,值元素仍为老对象的内存
- 深拷贝,值传递;既创建了新对象地址,值元素为新对象的内存
以及,按照之前的经验,我们认为切片处理的结果,都是新拷贝一片内存赋值过去。
切片拷贝是深还是浅?
先回顾下《简明Python教程》中引用一章所提的例子,
python
print('Simple Assignment')
shoplist = ['apple', 'mango', 'carrot', 'banana']
# mylist 只是指向同一对象的另一种名称
mylist = shoplist
# 我购买了第一项项目, 所以我将其从列表中删除
del shoplist[0]
print('shoplist is', shoplist)
print('mylist is', mylist)
# 注意到 shoplist 和 mylist 二者都
# 打印出了其中都没有 apple 的同样的列表, 以此我们确认
# 它们指向的是同一个对象
print('Copy by making a full slice')
# 通过生成一份完整的切片制作一份列表的副本
mylist = shoplist[:]
# 删除第一个项目
del mylist[0]
print('shoplist is', shoplist)
print('mylist is', mylist)
# 注意到现在两份列表已出现不同
此例中,我们可以看出,切片是复制了一片新内存给变量mylist,而第4行变量名字赋值,则只是传递的对象引用,并未申请新内存。
但我们就此可以认为切片操作是深拷贝吗?错把浅拷贝当深拷贝,请看下例。
python
# 切片操作是深拷贝,还是浅拷贝?
lst = [1,2,[3,4]]
l1 = lst[:]
lst[2][0] = 1
lst[1] = 0
print(l1)
lst = [1,2,3,4]
l1 = lst[:]
lst[2] = 1
print(l1)
Output:
[1, 2, [1, 4]]
[1, 2, 3, 4]
上例看出,列表中的切片仅拷贝了第1层的内存是赋值,改变原始列表,第6行中的[3,4]新变量中值就被修改成了[1,4],但显然改变lst[1]是新变量值就没变的,而第11行中lst原始值并未被改变。
所以,可以说切片是拷了,但只拷了一点点,本质等同于浅拷贝。
深拷贝和浅拷贝到底有啥区别?
- 浅拷贝,值引用;只创建了新对象地址,值元素仍为老对象的内存
- 深拷贝,值传递;既创建了新对象地址,值元素为新对象的内存
下面结合切片及深拷贝、浅拷贝举些实际场景用例来感受下以上概念内涵,结果见注释。
python
# 切片深拷贝还是浅拷贝实验
import numpy as np
import copy
scale = 10
# [0, 100],划分为5份
x = np.linspace(start=0, stop=100, num=5)
# x = np.linspace(0, 100, 5)
# print(x)
# 引用
y_np_vector = x[:] # numpy数据结构切片,未拷贝新内存,数据内存是直接引用的
y_np_scalar = x[:] # y_np和x指向的数据内存均相同
# print(id(x)) # x的对象地址不同于y,但是两者对象所指的数据均一致。要修改该浅拷贝,需用深拷贝得到一个新内存。
# # 验证如下
# x[0] = 100
# print(y_np_vector) # y_np_vector[0]变为100
# print(y_np_scalar) # y_np_scalar[0]变为100
# 赋值
y_np_vector = y_np_vector / scale # 新拷贝了一片内存,y_np_vector 结果指向新内存地址
# 引用拷贝
for i in range(len(y_linear)):
y_np_scalar[i] = y_np_scalar[i] / scale # 指向仍为x原内存
print('np切片,矢量处理与标量处理,x, y_np_vector, y_np_scalar')
print(x)
print(y_np_vector)
print(y_np_scalar)
# 转成内置列表切片,新申请一片内存赋值
x = np.linspace(start=0, stop=100, num=5)
y_list = x.tolist() # y_list已是新内存
y_res_list = y_list[:] # y_res_list新内存
for i in range(len(y_res2)):
y_res_list[i] = y_res_list[i] / scale # y_res_list已为新内存
print('np转为列表切片,x, y_list, y_res_list')
print(x)
print(y_list)
print(y_res_list)
# 显式进行赋值拷贝
x = np.linspace(start=0, stop=100, num=5)
y_res_copy = copy.copy(x) # 仅拷贝第1层内存,已足够
y_res_deep = copy.deepcopy(x) # 拷贝所有x的内部嵌套结构到新内存
for i in range(len(y_res2)):
y_res_copy[i] = y_res_copy[i] / scale # y_res_list已为新内存
y_res_deep[i] = y_res_deep[i] / scale # y_res2已为新内存
print('深浅拷贝,x, y_res_copy, y_res_deep')
print(x)
print(y_res_copy)
print(y_res_deep)
p切片,矢量处理与标量处理,x, y_np_vector, y_np_scalar
[10. 2.5 5. 7.5 10. ]
[10. 2.5 5. 7.5 10. ]
[10. 2.5 5. 7.5 10. ]
np转为列表切片,x, y_list, y_res_list
[ 0. 25. 50. 75. 100.]
[0.0, 25.0, 50.0, 75.0, 100.0]
[0.0, 2.5, 5.0, 7.5, 10.0]
深浅拷贝,x, y_res_copy, y_res_deep
[ 0. 25. 50. 75. 100.]
[ 0. 2.5 5. 7.5 10. ]
[ 0. 2.5 5. 7.5 10. ]
以上用例可得到以下结论:
- numpy中的数组切片,未生成新的副本,均为引用
- list列表中自带数据类型切片,仅拷贝第一层数据,等效于浅拷贝
- 仅一层数据结构时,深拷贝等效于浅拷贝
so,下面例子是否能分清拷贝的深浅呢?
python
import numpy as np
# 获取数据
row = 2
col = 3
scalar = 32
start = 0
stop = 12
num = 6
step = (stop - start) / (num-1)
x = np.linspace(start, stop, num) # [start, stop]
# num = (stop - start) / step + 1
# 切片与拷贝
y = x[:]
arr_2d_base = np.zeros([row, col])
arr_2d_scale = np.zeros([row, col])
for i in range(row):
arr_2d_base[i] = x[i*col:(i+1)*col] / scalar
arr_2d_scale[i] = arr_2d_base[i] * (2**8)
y[i*col:(i+1)*col] = y[i*col:(i+1)*col] / scalar
print('y是浅拷贝吗?')
print(x)
print(y)
# 二维操作
arr_2d_base = np.zeros([row, col])
for i in range(row):
arr_2d_base[i] = x[i*col:(i+1)*col] / scalar
arr_2d_scale = arr_2d_base * (2**8)
print('arr_2d_scale是深拷贝吗?')
print(arr_2d_scale)
arr_2d_base[0][0] = 1
print(arr_2d_scale)
本文小结
理论上最优的是默认拷贝新一片内存后修改,而不是原地修改。但工程上实现时,代价太大,且不是所有的都需要拷贝保留。
所以,在不同语言上,C都是手动管理内存,好在是明晰的告诉你,这个是原地修改还是新内存拷贝修改。Python由于是自动分配内存,就需要更深入的了解,每个拷贝赋值背后的内存结果,拷贝是引用还是赋值,赋值中又是深拷贝还是浅拷贝,
核心如下:
- 列表切片操作是浅拷贝,numpy中切片是引用
- 只有一层对象时,浅拷贝和深拷贝效果一致,都是新申请一片内存赋值。
- 切片是深拷贝吗?不是,自带列表类型中,都是第一层浅拷贝。
参考资料: