- 浅拷贝,指的是重新分配一块内存,创建一个新的对象,但里面的元素是原对象中各个子对象的引用。
- 深拷贝,是指重新分配一块内存,创建一个新的对象,并且将原对象中的元素,以递归的方式,通过创建新的子对象拷贝到新对象中。因此,新对象和原对象没有任何关联。
- 赋值:把原对象的引用赋值给新对象,二者指向的是内存中相同的地址
- 使用数据类型本身的构造器
- 对于可变的序列,还可以通过切片操作符:来完成浅拷贝
- Python 还提供了对应的函数 copy.copy() 函数,适用于任何数据类型
>>> list_1 = [1, 2, 3]
>>> list_2 = list(list_1)
>>> list_1 == list_2
True
>>> list_1 is list_2
False
>>> set_1 = {1, 2, 3}
>>> set_2 = set(set_1)
>>> set_1 == set_2
True
>>> set_1 is set_2
False
>>> tuple_1 = (1, 2, 3)
>>> tuple_2 = tuple(tuple_1)
>>> tuple_1 == tuple_2
True
>>> tuple_1 is tuple_2
True # tuple与其他数据类型不同, 不会创建一份浅拷贝,相反它会返回一个指向相同元组的引用(等价于赋值)
>>> dict_1 = {1: "a", 2: "b", 3: "c"}
>>> dict_2 = dict(dict_1)
>>> dict_1 == dict_2
True
>>> dict_1 is dict_2
False
is与==的区别
- ==是看前后两个变量的值是否相等(即二者是否指向同一个地址)
- is是看二者的id是否相等(即二者是否是同一个变量)
因为浅拷贝会为新变量重新分配一块内存,所以这两个变量的id不同,但二者的数值是相同的,说明二者均指向同一个地址
1.2.2 [仅限list] 使用切片操作来完成浅拷贝>>> list_1 = [1, 2, 3] >>> list_2 = list_1[:] >>> list_1 == list_2 True >>> list_1 is list_2 False1.2.3 [不限制数据类型] 使用copy.copy()函数完成浅拷贝
>>> import copy
>>> list_1 = [1, 2, 3]
>>> list_2 = copy.copy(list_1)
>>> list_1 == list_2
True
>>> list_1 is list_2
False
>>> set_1 = {1, 2, 3}
>>> set_2 = copy.copy(set_1)
>>> set_1 == set_2
True
>>> set_1 is set_2
False
>>> tuple_1 = (1, 2, 3)
>>> tuple_2 = copy.copy(tuple_1)
>>> tuple_1 == tuple_2
True
>>> tuple_1 is tuple_2
True # tuple与其他数据类型不同, 不会创建一份浅拷贝,相反它会返回一个指向相同元组的引用(等价于赋值)
>>> dict_1 = {1: "a", 2: "b", 3: "c"}
>>> dict_2 = copy.copy(dict_1)
>>> dict_1 == dict_2
True
>>> dict_1 is dict_2
False
1.2.4 tuple()没有浅拷贝
对于元组,不管是 tuple() 还是切片操作符:,亦或是copy.copy()均不会创建一份浅拷贝,相反它会返回一个指向相同元组的引用(即等价于赋值)
>>> tuple_1 = (1, 2, 3)
>>> tuple_2 = tuple(tuple_1)
>>> tuple_3 = tuple_1[:]
>>> tuple_4 = copy.copy(tuple_1)
>>> tuple_5 = tuple_1
>>> print(f"{tuple_1 == tuple_2}n{tuple_1 == tuple_3}n{tuple_1 == tuple_4}n{tuple_1 == tuple_5}")
True
True
True
True
>>> print(f"{tuple_1 is tuple_2}n{tuple_1 is tuple_3}n{tuple_1 is tuple_4}n{tuple_1 is tuple_5}")
True
True
True
True
1.2.5 字符串也没有浅拷贝
和tuple类似:
>>> str_1 = "PyTorch"
>>> str_2 = str(str_1)
>>> str_3 = str_1[:]
>>> str_4 = copy.copy(str_1)
>>> str_5 = str_1
>>> print(f"{str_1 == str_2}n{str_1 == str_3}n{str_1 == str_4}n{str_1 == str_5}")
True
True
True
True
>>> print(f"{str_1 is str_2}n{str_1 is str_3}n{str_1 is str_4}n{str_1 is str_5}")
True
True
True
True
也就是说,对字符串和元组使用 copy()、[:]、本身的构造器(tuple(), str())完成的复制,都只是开辟了内存存储原对象的引用,而不是存储原对象的子对象的引用。
1.2.6 浅拷贝和赋值的本质区别,赋值只是把原对象的引用给到新对象>>> list_1 = [1, 2, 3]
>>> list_2 = list_1
>>> list_3 = copy.copy(list_1)
>>> print(f"{list_1 == list_2}n{list_1 == list_3}")
True
True
>>> print(f"{list_1 is list_2}n{list_1 is list_3}")
True
False
>>> print(f"{id(list_1)}n{id(list_2)}n{id(list_3)}")
140164275963200
140164275963200
140164275885952
可以看到,赋值的话,list_1和list_2这两个变量的id都相同,但浅拷贝不是,浅拷贝两个变量的id不同,但都指的是相同的内容。
1.2.7 对数据采用浅拷贝的方式时,如果原对象中的元素不可变,那倒无所谓;但如果元素可变,浅拷贝通常会出现一些问题 1.2.7.1 赋值>>> list_1 = [1, 2, 3]
>>> list_2 = list_1
>>> list_2.append("new value")
>>> print(f"list_1: {list_1}nlist_2: {list_2}")
list_1: [1, 2, 3, 'new value']
list_2: [1, 2, 3, 'new value']
两个列表的值都发生变化了!这是因为list_1, list_2都指向的是同一块内存中的数据,不管修改list_1还是修改list_2,其实本质上修改的都是相同的东西。
但对于不可变对象来说,没有这种顾虑:
>>> tuple_1 = (1, 2, 3)
>>> tuple_2 = tuple_1
>>> tuple_2 += tuple([1, 2, 3])
>>> print(f"tuple_1: {tuple_1}ntuple_2: {tuple_2}")
tuple_1: (1, 2, 3)
tuple_2: (1, 2, 3, 1, 2, 3)
1.2.7.1 浅拷贝
>>> list_1 = [1, 2, 3]
>>> list_2 = copy.copy(list_1)
>>> list_2.append("new value")
>>> print(f"list_1: {list_1}nlist_2: {list_2}")
list_1: [1, 2, 3]
list_2: [1, 2, 3, 'new value']
我们发现浅拷贝不会出现前面那种赋值的情况了!
是这样吗?
看下面的操作:
>>> list_1 = [[1, 2], [3, 4]]
>>> list_2 = copy.copy(list_1)
>>> list_2.append(100) # 这个操作没问题
>>> print(f"list_1: {list_1}nlist_2: {list_2}")
list_1: [[1, 2], [3, 4]]
list_2: [[1, 2], [3, 4], 100]
>>> # 仔细看下面的操作
>>> list_2[0].append("元素中append")
>>> print(f"list_1: {list_1}nlist_2: {list_2}")
list_1: [[1, 2, '元素中append'], [3, 4]]
list_2: [[1, 2, '元素中append'], [3, 4], 100]
看到了吗?浅拷贝只能保证最外面的那层壳,不能保护里面的元素。
这也就是为什么copy.copy()被成为浅拷贝,因为它拷贝的东西只有一层,里面层的东西就不拷贝了,直接跟赋值一样用引用实现。
为了解决浅拷贝只管一层的弊端,我们可以使用深拷贝。
1.3 Python中的深拷贝深拷贝不同于浅拷贝只负责一层,深拷贝负责每一层!
>>> list_1 = [[1, 2], [3, 4]]
>>> list_2 = copy.deepcopy(list_1)
>>> list_2.append(100) # 这个操作深拷贝和浅拷贝是没有区别的,都会负责这一层
>>> print(f"list_1: {list_1}nlist_2: {list_2}")
list_1: [[1, 2], [3, 4]]
list_2: [[1, 2], [3, 4], 100]
>>> # 第二层
>>> list_2[0].append("元素中append")
>>> print(f"list_1: {list_1}nlist_2: {list_2}")
list_1: [[1, 2], [3, 4]]
list_2: [[1, 2, '元素中append'], [3, 4], 100]
我们多构建几层看看:
>>> list_1 = [[[[1,2],[3,4]], [5, 6]], [7, 8]]
>>> list_2 = copy.deepcopy(list_1)
>>> print(f"list_1: {list_1}nlist_2: {list_2}")
list_1: [[[[1, 2], [3, 4]], [5, 6]], [7, 8]]
list_2: [[[[1, 2], [3, 4]], [5, 6]], [7, 8]]
>>> # 在深层append元素
>>> list_2[0][0][0].append("New Value!")
>>> print(f"list_1: {list_1}nlist_2: {list_2}")
list_1: [[[[1, 2], [3, 4]], [5, 6]], [7, 8]]
list_2: [[[[1, 2, 'New Value!'], [3, 4]], [5, 6]], [7, 8]]
1.4 Python中赋值、浅拷贝、深拷贝的总结
- 赋值=:最不靠谱的操作,对于list来说,牵一发而动全身
- 浅拷贝copy.copy():相比赋值操作,能拷贝一层的数据,但不拷贝第二层以后的数据
- 深拷贝copy.deepcopy():对于list来说,能用这个就用这个
简单理解,numpy中没有浅拷贝,只有赋值和深拷贝。
2.2 赋值>>> a = np.array([1, 2, 3])
>>> b = a
>>> print(f"a: {a}nb: {b}")
a: [1 2 3]
b: [1 2 3]
>>> b[0] = 789
>>> print(f"a: {a}nb: {b}")
a: [789 2 3]
b: [789 2 3]
可以看到,和Python的赋值一样,对于list来说,一层都保护不了。
2.3 深拷贝和Python的深拷贝一样,但是语法不同:
- Python中的深拷贝:a = copy.deepcopy(数据)
- Numpy中的深拷贝:a = 数据.copy()
看一下numpy中赋值=和深拷贝copy()的区别:
>>> import torch
>>> import numpy as np
>>> a = torch.zeros(1, 1, 1)
>>> a = a.numpy()
>>> a
array([[[0.]]], dtype=float32)
>>> b = a # 赋值
>>> c = a.copy() # 深拷贝
>>> b[0][0][0] = 1024
>>> c[0][0][0] = 2048
>>> print(f"a: {a}nb: {b}nc: {c}")
a: [[[1024.]]]
b: [[[1024.]]]
c: [[[2048.]]]
很明显,numpy中的深拷贝和Python中的深拷贝效果是一样的。
3. PyTorch中的浅拷贝、深拷贝以及其他注意事项 3.1 浅拷贝 —— torch.from_numpy()和torch.as_tensor()torch.from_numpy和torch.as_tensor是浅拷贝,在内存中共享数据。
例子1:
>>> import torch
>>> import numpy as np
>>> a = np.array([1, 2, 3])
>>> a_1 = torch.from_numpy(a) # 浅拷贝
>>> a_2 = torch.as_tensor(a) # 浅拷贝
>>> a_1[0] = 1000
>>> a_2[1] = 2000
>>> print(f"a: {a}n a_1: {a_1}n a_2: {a_2}")
a: [1000 2000 3]
a_1: tensor([1000, 2000, 3])
a_2: tensor([1000, 2000, 3])
例子2:
>>> import torch
>>> import numpy as np
>>> a = np.array([[1,2], [3,4]])
>>> a_1 = torch.from_numpy(a)
>>> a_2 = torch.as_tensor(a)
>>> a_1[0,0] = 1000
>>> a_2[0,1] = 2000
>>> print(f"a: {a}n a_1: {a_1}n a_2: {a_2}")
a: [[1000 2000]
[ 3 4]]
a_1: tensor([[1000, 2000],
[ 3, 4]])
a_2: tensor([[1000, 2000],
[ 3, 4]])
我们可以看到,torch.from_numpy和torch.as_tensor的确是浅拷贝。
3.2 深拷贝 —— torch.Tensor()和torch.tensor()torch.Tensor和torch.tensor是深拷贝,在内存中创建一个额外的数据副本,不共享内存,所以不受数组改变的影响。
例子1:
>>> import torch
>>> import numpy as np
>>> a = np.array([1, 2, 3])
>>> a_1 = torch.Tensor(a) # 深拷贝
>>> a_2 = torch.tensor(a) # 深拷贝
>>> a_1[0] = 1000
>>> a_2[1] = 2000
>>> print(f"a: {a}n a_1: {a_1}n a_2: {a_2}")
a: [1 2 3]
a_1: tensor([1000., 2., 3.])
a_2: tensor([ 1, 2000, 3])
例子2:
>>> import torch
>>> import numpy as np
>>> a = np.array([[1,2], [3,4]])
>>> a_1 = torch.Tensor(a) # 深拷贝
>>> a_2 = torch.tensor(a) # 深拷贝
>>> a_1[0, 0] = 1000
>>> a_2[0, 1] = 2000
>>> print(f"a: {a}na_1: {a_1}na_2: {a_2}")
a: [[1 2]
[3 4]]
a_1: tensor([[1000., 2.],
[ 3., 4.]])
a_2: tensor([[ 1, 2000],
[ 3, 4]])
我们可以看到,torch.tensor和torch.Tensor的确是深拷贝。
接下来介绍一些其他PyTorch需要注意的事项。
3.3 inplace=Trueinplace =True的意思是进行原地操作。举个例子:
x = x + 5 # 这是一个原地操作,可以节省内存 # ---------------------- y = x + 5 x = y # 与x = x + 5虽然功能相同,但更加费内存
3.4 tensor.detach()和tensor.clone()Note:在PyTorch中,你可以想 x = x + 5,但千万别写x += 5,这样很容易报错
- tensor.clone()是深拷贝,开辟新的存储地址而不是引用来保存旧的tensor
- tensor.clone()在梯度会传时充当中间变量,会将梯度传给源张量进行叠加,但是本身不保存其grad,值为None。
- tensor.detach是浅拷贝,新的tensor会脱离计算图,不会牵扯梯度计算。
例子:
>>> import torch
>>> x = torch.tensor([2., 4.], requires_grad=True)
>>> clone_x = x.clone() # 深拷贝
>>> detach_x = x.detach() # 浅拷贝
>>> clone_detach_x = x.clone().detach() # 先深拷贝再浅拷贝,最后为浅拷贝
>>>
>>> y = 2 * x + 10
>>> y.backward(torch.FloatTensor([1, 1])) # y进行反向传播,梯度值为[1,1]
>>>
>>> print(f"x.grad: {x.grad}nclone_x.requires_grad: {x.requires_grad}nclone_x.grad: {clone_x.grad}ndetach_x.requires_grad: {detach_x.requires_grad}nclone_detach_x.requires_grad: {clone_detach_x.requires_grad}")
x.grad: tensor([2., 2.])
clone_x.requires_grad: True # 深拷贝,也会拷贝其梯度状态
clone_x.grad: None # 但是不会存储梯度信息
detach_x.requires_grad: False # 浅拷贝不会拷贝其梯度状态(自然也没有梯度信息了)
clone_detach_x.requires_grad: False # 浅拷贝不会拷贝其梯度状态(自然也没有梯度信息了)
3.5 tensor.contiguous
在PyTorch中,很多操作都用到了浅拷贝的思路,只是重新定义下标与元素的对应关系。例如在使用transpose()进行转置操作时,PyTorch不会创建转置后的新的tensor,仅仅修改tensor中的一些属性(元数据)。转置和原数据的内存是共享的。
例子1:不使用tensor.contiguous()
>>> import torch
>>> x = torch.randn((3, 2))
>>> y = torch.transpose(x, 0, 1) # tensor x 的第0维和第1维进行转置,结果给y -> 浅拷贝
>>> print(f"x: {x}ny: {y}")
x: tensor([[ 0.4589, 1.3154],
[-0.4431, -0.6626],
[-0.7404, 1.9109]])
y: tensor([[ 0.4589, -0.4431, -0.7404],
[ 1.3154, -0.6626, 1.9109]])
>>> print(f"x.shape: {x.shape}ny.shape: {y.shape}")
x.shape: torch.Size([3, 2])
y.shape: torch.Size([2, 3])
>>> y[0, 0] = 1024
>>> print(f"x: {x}ny: {y}")
x: tensor([[ 1.0240e+03, 1.3154e+00], # 变了
[-4.4314e-01, -6.6265e-01],
[-7.4039e-01, 1.9109e+00]])
y: tensor([[ 1.0240e+03, -4.4314e-01, -7.4039e-01], # 变了
[ 1.3154e+00, -6.6265e-01, 1.9109e+00]])
>>> print(f"x.shape: {x.shape}ny.shape: {y.shape}")
x.shape: torch.Size([3, 2])
y.shape: torch.Size([2, 3])
说明torch.transpose()操作是浅拷贝。
例子2:使用tensor.contiguous()
>>> import torch
>>> x = torch.randn((3, 2))
>>> y = torch.transpose(x, 0, 1).contiguous() # 加了tensor.contiguous(),从浅拷贝变为深拷贝
>>> print(f"x: {x}ny: {y}")
x: tensor([[-0.5056, 0.4376],
[ 0.0163, 0.5593],
[-1.1808, 0.7779]])
y: tensor([[-0.5056, 0.0163, -1.1808],
[ 0.4376, 0.5593, 0.7779]])
>>>
>>> y[0, 0] = 11
>>> print(f"x: {x}ny: {y}")
x: tensor([[-0.5056, 0.4376], # 没变
[ 0.0163, 0.5593],
[-1.1808, 0.7779]])
y: tensor([[11.0000, 0.0163, -1.1808], # 变了
[ 0.4376, 0.5593, 0.7779]])
加了tensor.contiguous(),torch.transpose()从浅拷贝变为深拷贝。
3.6 tensor.view()我们看一下tensor.view()是浅拷贝还是深拷贝。
例子1: 不加tensor.contiguous()
>>> x = torch.randn(3, 2)
>>> y = x.view(2, 3)
>>> print(f"x: {x}ny: {y}")
x: tensor([[-1.6152, 2.3585],
[ 0.4665, 1.6596],
[ 1.1893, 0.3895]])
y: tensor([[-1.6152, 2.3585, 0.4665],
[ 1.6596, 1.1893, 0.3895]])
>>> y[0, 0] = 9
>>> print(f"x: {x}ny: {y}")
x: tensor([[9.0000, 2.3585],
[0.4665, 1.6596],
[1.1893, 0.3895]])
y: tensor([[9.0000, 2.3585, 0.4665],
[1.6596, 1.1893, 0.3895]])
说明tensor.view()也是浅拷贝。
例子2:加了tensor.contiguous()
>>> import torch
>>> x = torch.randn(3, 2)
>>> y = x.view(2, 3).contiguous()
>>> y[0, 0] = 9
>>> print(f"x: {x}ny: {y}")
x: tensor([[ 9.0000, -0.3736], # 变了
[ 1.9279, -0.1798],
[-0.1475, -1.0268]])
y: tensor([[ 9.0000, -0.3736, 1.9279], # 变了
[-0.1798, -0.1475, -1.0268]])
说明tensor.view不管加不加contiguous都是浅拷贝。
3.7 tensor.reshape我们看一下tensor.reshape()是浅拷贝还是深拷贝。
例子1:不加contiguous
>>> x = torch.randn(2, 3)
>>> y = x.reshape(3, 2)
>>> y[0,0] = 9
>>> print(f"x: {x}ny: {y}")
x: tensor([[ 9.0000, -0.2499, 1.4942],
[-0.4103, -2.7727, 0.4829]])
y: tensor([[ 9.0000, -0.2499],
[ 1.4942, -0.4103],
[-2.7727, 0.4829]])
tensor.reshape本身是一个浅拷贝。
例子2:加了tensor.contiguous:
>>> import torch
>>> x = torch.randn(2, 3)
>>> y = x.reshape(3, 2).contiguous()
>>> y[0,0] = 9
>>> print(f"x: {x}ny: {y}")
x: tensor([[ 9.0000, -0.7157, 0.0864],
[-1.3975, 0.8973, -0.5598]])
y: tensor([[ 9.0000, -0.7157],
[ 0.0864, -1.3975],
[ 0.8973, -0.5598]])
仍然是一个浅拷贝。
3.8 tensor.permute()例子1:不加contiguous
>>> import torch
>>> x = torch.randn(2, 3)
>>> y = x.permute(1, 0)
>>> y[0,0] = 9
>>> print(f"x: {x}ny: {y}")
x: tensor([[ 9.0000, -1.2463, -1.3497],
[-0.2729, -0.6057, -1.0876]])
y: tensor([[ 9.0000, -0.2729],
[-1.2463, -0.6057],
[-1.3497, -1.0876]])
tensor.permute本身是一个浅拷贝。
例子2:加了tensor.contiguous:
>>> import torch
>>> x = torch.randn(2, 3)
>>> y = x.permute(1, 0).contiguous()
>>> y[0,0] = 9
>>> print(f"x: {x}ny: {y}")
x: tensor([[-0.6026, -0.7970, 1.0808],
[-1.4109, -1.2785, 1.5553]])
y: tensor([[ 9.0000, -1.4109],
[-0.7970, -1.2785],
[ 1.0808, 1.5553]])
加了contiguous之后,permute变为深拷贝。
3.9 总结PyTorch很多操作都是浅拷贝,加contiguous有的可以将其变为深拷贝,有的不行,慢慢摸索吧



