Python中的拷贝赋值原理及其示例

  • Python中的b=a浅拷贝赋值问题
  • 当a为单个个体
  • a为整型
  • a为浮点型
  • 当a为列表
  • 局部改变
  • 全局改变
  • Python中的浅和深拷贝赋值问题
  • 浅拷贝
  • 深拷贝
  • 一维列表
  • 多维列表



我将在本章内容中对b=a的赋值原理进行阐述。

Python中的b=a浅拷贝赋值问题

今天在研究Tranformer的PositionalEncoding代码时,我遇到了一个表面看起来非常简单的问题:如果我执行赋值语句b=a,那么什么情况下b改变,a跟着改变;什么情况下b改变,a不变;什么情况下a改变,b跟着改变;什么情况下a改变,b不变呢?我在这个问题里思考了良久,终于从局部改变和全局改变出发,总结出了自己的一点看法。有一种解释,Python在处理整型和字符型时时使用值传递;而在处理浮点型、列表等类型时使用地址传递的。但是,从我的角度来看,都是在通过地址传递,只是全局和局部改变这两种不同的改变方式的问题。我所认为的赋值原理为:当你对b中的元素进行进行局部改变时,那b的指针所指的内存空间是不变的,a将会随着b的变化而变化;当你对b中的元素进行进行全局改变时,那b的指针所指的内存空间将会发生改变,a和b将不再具有相同的内存空间,a也就不随b的变化而变化。具体情况如下所示。

提前声明:在python中,我们所采用的变量名相当于一个指针。在我们为变量进行赋值时,相当于是将指针指向了这个值的内存空间。同时,id() 函数用于获取对象的内存地址。

当a为单个个体

这里的单个个体指的是,例如:a = 1, a = 0.1, a = '1’等等,这种只有一个元素的单个个体。这时对于元素的单个个体来说,局部便为全局,所以无论如何对b进行操作(除了让其等于它自己),均改变了b的指针所指的内存空间,此时a便不随b改变而改变。此时,我们对b进行赋值操作。

a为整型

a = 1
b = a
print(a,b)			# 输出为: 1 1
print(id(a))		# 输出为: 140720003751680
print(id(b))		# 输出为: 140720003751680

b = a
b = b+1
print(a,b)			# 输出为: 1 2
print(id(a))		# 输出为: 140720003751680
print(id(b))		# 输出为: 140720003751712

b = a
b = b*3
print(a,b)			# 输出为: 1 2
print(id(a))		# 输出为: 140720003751680
print(id(b))		# 输出为: 140720003751744

但是,发现了一个新的问题。我发现当b的数值一样时,无论b的计算思路如何,所得到的内存地址是一致的。具体情况如下。

a = 1
b = a
b = b+1
print(a,b)			# 输出为: 1 2
print(id(a))		# 输出为: 140720003751680
print(id(b))		# 输出为: 140720003751712

b = b*2
print(a,b)			# 输出为: 1 2
print(id(a))		# 输出为: 140720003751680
print(id(b))		# 输出为: 140720003751712

c = 2
print(id(c))		# 输出为: 140720003751712

这就说明一个问题,当一个变量被赋值时,会出现一个常量,常数是不可变类型,此时,会对常量分配内存空间。如果再出现一个变量等于此常量时,这个新变量的指针也指向了这个常量的空间,所以会出现无论通过何种计算,只要得到的结果相同,变量指针所指的内存空间都是一致的。

a为浮点型

a = 0.1
b = a
print(a,b)			# 输出为: 0.1 0.1
print(id(a))		# 输出为: 2214226455856
print(id(b))		# 输出为: 2214226455856

b = a
b = b+0.5
print(a,b)			# 输出为: 0.1 0.6
print(id(a))		# 输出为: 2214226455856
print(id(b))		# 输出为: 2214226064720

b = a
b = b*3
print(a,b)			# 输出为: 0.1 0.3
print(id(a))		# 输出为: 2214226455856
print(id(b))		# 输出为: 2214226064752

从这两个示例我们就可以看出,对于单个个体,无论是整型还是浮点型,对于其的操作均为全局改变,所以b改变之后其指针所指的内存地址便会发生改变,则a不会随着b的变化而变化。同时证明了

当a为列表

当a为列表(list)时,便会牵涉到局部改变和全局改变的差异。我将从这两个方向进行分析。

局部改变

当对b进行局部改变时,其指针所指的内存地址不会改变,a会随着b的改变而改变。实际案例如下。

示例一:

a = [0,1,2,3]
b = a
print(a)			# 输出为: [0,1,2,3]
print(b)			# 输出为: [0,1,2,3]
print(id(a))		# 输出为: 1918053456960
print(id(b))		# 输出为: 1918053456960

# 当发生局部改变时,其指针所指的内存地址并没有发生改变,所以当b改变时,a也同时跟随改变。
b = a
b[1] = 2
print(a)			# 输出为: [0,2,2,3]
print(b)			# 输出为: [0,2,2,3]
print(id(a))		# 输出为: 1918053456960
print(id(b))		# 输出为: 1918053456960

b[1] = b[2]+1
print(a)			# 输出为: [0,3,2,3]
print(b)			# 输出为: [0,3,2,3]
print(id(a))		# 输出为: 1918053456960
print(id(b))		# 输出为: 1918053456960

b.append(3)
print(a)			# 输出为: [0,3,2,3,3]
print(b)			# 输出为: [0,3,2,3,3]
print(id(a))		# 输出为: 1918053456960
print(id(b))		# 输出为: 1918053456960

示例二:

a = [[0,1,2,3],[0,1,2]]
b = a
print(a)			# 输出为: [[0, 1, 2, 3], [0, 1, 2]]
print(b)			# 输出为: [[0, 1, 2, 3], [0, 1, 2]]
print(id(a))		# 输出为: 1918050897152
print(id(b))		# 输出为: 1918050897152

# 当发生局部改变时,其指针所指的内存地址并没有发生改变,所以当b改变时,a也同时跟随改变。
b = a
b[1].append(2)
print(a)			# 输出为: [[0, 1, 2, 3], [0, 1, 2, 2]]
print(b)			# 输出为: [[0, 1, 2, 3], [0, 1, 2, 2]]
print(id(a))		# 输出为: 1918050897152
print(id(b))		# 输出为: 1918050897152

b = a
b.append([0,1])
print(a)			# 输出为: [[0, 1, 2, 3], [0, 1, 2, 2], [0, 1]]
print(b)			# 输出为: [[0, 1, 2, 3], [0, 1, 2, 2], [0, 1]]
print(id(a))		# 输出为: 1918050897152
print(id(b))		# 输出为: 1918050897152

b = a
b[1] = [0,1]
print(a)			# 输出为: [[0, 1, 2, 3], [0, 1], [0, 1]]
print(b)			# 输出为: [[0, 1, 2, 3], [0, 1], [0, 1]]
print(id(a))		# 输出为: 1918050897152
print(id(b))		# 输出为: 1918050897152

全局改变

当对b进行全局改变时,其指针所指的内存地址会发生改变,a不会随着b的改变而改变。实际案例如下。
示例一:

a = [0,1,2]
b = a
print(a)			# 输出为: [0, 1, 2]
print(b)			# 输出为: [0, 1, 2]
print(id(a))		# 输出为: 1918053336128
print(id(b))		# 输出为: 1918053336128

# 当发生全局改变时,其指针所指的内存地址发生改变,所以当b改变时,a没有跟随改变。
b = a
b = [0,1]
print(a)			# 输出为: [0, 1, 2]
print(b)			# 输出为: [0, 1]
print(id(a))		# 输出为: 1918053336128
print(id(b))		# 输出为: 1918050891584

b = a
b = b+[1]
print(a)			# 输出为: [0, 1, 2]
print(b)			# 输出为: [0, 1, 2, 1]
print(id(a))		# 输出为: 1918053336128
print(id(b))		# 输出为: 1918050591040

示例二:

a = [[0,1,2],[0,1,2]]
b = a
print(a)			# 输出为: [[0, 1, 2], [0, 1, 2]]
print(b)			# 输出为: [[0, 1, 2], [0, 1, 2]]
print(id(a))		# 输出为: 1918053507200
print(id(b))		# 输出为: 1918053507200

# 当发生全局改变时,其指针所指的内存地址发生改变,所以当b改变时,a没有跟随改变。
b = a
b = b+[1]
print(a)			# 输出为: [[0, 1, 2, 3], [0, 1, 2, 2]]
print(b)			# 输出为: [[0, 1, 2, 3], [0, 1, 2, 2], 1]
print(id(a))		# 输出为: 1918053507200
print(id(b))		# 输出为: 1918053513920

Python中的浅和深拷贝赋值问题

b=a浅拷贝我们在第一个部分做了非常详细的解释。现在对浅和深拷贝赋值问题进行阐述。

浅拷贝

b=a将两者引用一个对象,即指向同一块内存地址,相当于b对a进行浅拷贝。在这种情况下,对这块内存地址的内容进行修改将同时改变a和b。所以一般我们在进行实验的时候不推荐使用这种方法。

a = [1,2,3]
b = a
a.append(4)
print(a)			# 输出为: [1, 2, 3, 4]
print(b)			# 输出为: [1, 2, 3, 4]

深拷贝

为了避免以上两者一起变化的情况,我们采用深拷贝来隔离a和b,让他俩具有相同的内容却具有不同的内存地址。可以说两者除了长得一样并没有其他关系,这也是比较常用的拷贝方式。

一维列表

对于一维列表,我们可以用以下的方法来对数据进行深拷贝。我们发现进行深拷贝之后,a和b指针所指的内存地址不同。

a = [1,2,3]
b = a[:]
a.append(4)
print(a)			# 输出为: [1, 2, 3, 4]
print(b)			# 输出为: [1, 2, 3]
print(id(a))		# 输出为: 1918041829184
print(id(b))		# 输出为: 1918053513920

多维列表

对于二维列表我们再采用上述方法进行拷贝,所得到的结果便不同了。虽然我们使用了b=a[:]来完成对a的深拷贝,但是二维列表中a[0]和b[0]的内存地址却是一样的,及a[0]和b[0]指针所指的内存地址是一样的。说明b=a[:]只能深拷贝最外一层的内存地址,内部所指的依旧是同一个内存地址。

a = [[1]]
b = a[:]
print(id(a))		# 输出为: 1918053510336
print(id(b))		# 输出为: 1918053508992
print(id(a[0]))		# 输出为: 1918053508224
print(id(b[0]))		# 输出为: 1918053508224

所以,当我们对多维列表进行修改时,我们最好使用copy.deepcopy函数来对数据进行赋值。

import copy
a = [[1]]
b = copy.deepcopy(a)
print(id(a))		# 输出为: 1918053510336
print(id(b))		# 输出为: 1918053508992
print(id(a[0]))		# 输出为: 1918053508224
print(id(b[0]))		# 输出为: 1918053503424