python 中的拷贝与赋值

今天我们来讨论一个看似简单的问题。这篇内容产生于我对pythoncopy库中的deepcopy()方法的使用过程,在微调模型和进行多轮对话时,我们经常需要对模型的历史参数或是历史对话记录进行保存,按常理来说,直接用=赋值给另一个变量就好了,但实际项目中往往使用deepcopy()方法对一些“复杂变量”进行保存,今天我们就一起来探索一下python中的拷贝与赋值。

python 中的对象类型

在 Python 中,对象分为可变对象和不可变对象。了解这些对象的区别对于编写高效且正确的代码非常重要。

不可变对象

不可变对象一旦创建,无法在原地修改。如果需要修改它们的值,只能通过创建一个新的对象并重新赋值。

常见的不可变对象包括:

整数 (int)

  • 例如:a = 10。不能在原地修改 10 的值。

浮点数 (float)

  • 例如:b = 3.14。类似于整数,浮点数也是不可变的。

字符串 (str)

  • 例如:s = "hello"。字符串是不可变的,修改字符串中的字符会创建一个新的字符串对象。

元组 (tuple)

  • 例如:t = (1, 2, 3)。元组中的元素不能被修改或重新赋值。

布尔值 (bool)

  • 例如:flag = True。布尔值也是不可变的。

frozenset

  • 例如:fs = frozenset([1, 2, 3])。这是一个不可变的集合。

可变对象

可变对象在创建后可以修改其内容,而不需要创建新的对象。这使得它们在某些操作中更为高效,但也可能带来意外的副作用。

常见的可变对象包括:

列表 (list)

  • 例如:lst = [1, 2, 3]。可以通过 lst[0] = 10 来修改列表中的元素。

字典 (dict)

  • 例如:d = {'a': 1, 'b': 2}。可以通过 d['a'] = 10 来修改字典中的键值对。

集合 (set)

  • 例如:s = {1, 2, 3}。可以通过 s.add(4) 来修改集合的内容。

字节数组 (bytearray)

  • 例如:ba = bytearray([1, 2, 3])。可以通过 ba[0] = 0 来修改字节数组中的值。

可变与不可变的对比

  • 内存使用:不可变对象通常在内存中是唯一的,多个引用可以共享同一个对象(尤其是小的整数和短字符串)。可变对象因为可以修改,通常不会被共享。
  • 副作用:可变对象的修改会影响所有引用它的变量,而不可变对象则不会有这种副作用。

举例说明

# 不可变对象示例
a = "hello"
b = a
b = "world"
print(a)  # 输出: "hello"(a 未受 b 的影响)

# 可变对象示例
x = [1, 2, 3]
y = x
y[0] = 10
print(x)  # 输出: [10, 2, 3](x 受到了 y 的修改影响)

在第一个示例中,ab 最初指向同一个字符串 "hello",但当 b 被重新赋值为 "world" 时,它不再指向 a 的对象,因此 a 保持不变。

在第二个示例中,xy 最初指向同一个列表。当 y 修改列表时,由于列表是可变的,因此 x 也受到了影响。

到这里我们明确了一个关于赋值的问题,如果你将一个变量a赋值为“不可变对象”(字符串、元组、数字),那么你可以毫无顾忌的使用另一个变量b来保留a变量的内容,不需要担心这种直接的赋值b = a会影响a变量的内容;但是如果将变量a赋值为“不可变对象”(列表、字典、集合),你的变量传递就是无效的,虽然你写b = a的本意是用新的变量b来保护变量a中的内容,这样你就可以肆无忌惮的改动变量ab了,但事实并非如此。

Python 中的深拷贝和浅拷贝

如果只介绍到上面就结束了,你可能会说:这谁不知道啊!

实际上,与变量赋值相关联的还有拷贝这个概念,如果你学过C++的话,你就会了解深拷贝和浅拷贝,下面我简单回顾一下:

第一种拷贝赋值的方法叫做深拷贝,第二种拷贝赋值的方法叫做浅拷贝

  • 深拷贝:先创建一份新的空间来容纳数据蓝本,然后再把实际内容拷贝过去。数据内容相同,但放在两块不同的内存空间。
  • 浅拷贝:定义一个新的变量名,然后将其指向另一个变量所对应的内存块,本质上就是给同一块数据起了个“外号”。

在 Python 中也是一样,对存有“可变对象”的变量a执行b = a,其实就是一种浅拷贝,两个变量名指向同一块数据(数组、列表、集合),“一荣俱荣,一损俱损”;如果变量a内是“不可变对象”的话(字符串、元组、数字),那没办法,虽然变量b也想图省事直接把a的数据拿过来就用,但是“不可变对象”不能复用,他只能另起炉灶(内存空间)了。

再理解了赋值、深拷贝、浅拷贝之后,回到我们的主题,为什么python重要专门写一个copy库,包含浅拷贝copy()和深拷贝deepcopy()两种拷贝方法呢?

这是因为在某些场景(模型微调需要保存历史参数、模型推理需要保存历史对话记录)中,我们往往需要保存”可变类型对象“,如果只用b = a来实现的话,就会导致”虚假“的拷贝,我们以为保存好了原来的模型参数,开开心心微调完,发现模型调傻了,又没用deepcopy()深拷贝,想回到过去也回不去了。。。

Python 中浅拷贝与赋值的区别

看到这里,你可能回想,那你这么说,浅拷贝b = copy.copy(a)和赋值b = a不是一样了吗?实则不然。浅拷贝和直接赋值在某些情况下效果相同,但在处理复杂数据结构(如嵌套的列表或字典)时,它们的行为会有所不同。

直接赋值

直接赋值时,两个变量指向同一个对象。如果你对其中一个变量进行修改,另一个变量也会受到影响,因为它们共享相同的内存引用。

示例
a = [[1, 2, [5,6]], [3, 4]]
b = a  # 直接赋值

b[0][0] = 88
print(a)  # 输出: [[88, 2, [5, 6]], [3, 4]]
print(b)  # 输出: [[88, 2, [5, 6]], [3, 4]]
a = [[1, 2, [5,6]], [3, 4]]
b = a  # 直接赋值

b[0] = 99
print(a)  # 输出: [99, [3, 4]]
print(b)  # 输出: [99, [3, 4]]

在这个例子中,ab 指向同一个列表对象。对 b 的修改直接反映在 a 上。

浅拷贝

浅拷贝会创建一个新对象,但它只拷贝对象的第一层元素,嵌套的子元素仍然是引用同一对象。因此,浅拷贝对象的顶层结构是独立的,但内部的嵌套元素仍然共享。

示例
import copy

a = [[1, 2, [5,6]], [3, 4]]
b = copy.copy(a)  # 浅拷贝

b[0][0] = 88
print(a)  # 输出: [[88, 2, [5, 6]], [3, 4]]
print(b)  # 输出: [[88, 2, [5, 6]], [3, 4]]

到这里看起来直接赋值浅拷贝的结果一样吧,不过别急

import copy

a = [[1, 2, [5,6]], [3, 4]]
b = copy.copy(a)  # 浅拷贝

b[0] = 99
print(a)  # 输出: [[1, 2, [5, 6]], [3, 4]]
print(b)  # 输出: [99, [3, 4]]

这里怎么就突然不一样了呢?

在这个例子中,ab 是两个不同的列表对象(列表的第一维),但它们的子元素(第二维、第三维。。。)仍然引用同一个子列表。因此,修改 b[0][0] 也会影响 a[0][0]

直接赋值 vs 浅拷贝

  • 直接赋值:两个变量指向同一个对象,任何一个变量的修改都会影响另一个变量。
  • 浅拷贝:创建了一个新的对象,但内部的嵌套元素仍然是共享的,修改这些嵌套元素会影响原始对象。

何时有区别?

  • 简单对象(如数字、字符串):浅拷贝和直接赋值的效果一样,因为这些对象是不可变的。
  • 复杂对象(如列表、字典,特别是嵌套的结构):浅拷贝和直接赋值效果不同,浅拷贝创建了新的顶层对象,但嵌套元素仍然共享。

总结

  • 直接赋值:完全共享相同的对象,任何改变都会反映在所有引用中。
  • 浅拷贝:创建一个新的对象,但嵌套的子对象仍然共享。因此,顶层修改不会影响原始对象,但修改嵌套对象会。

如果你需要一个完全独立的副本,包括所有嵌套的对象,你需要使用深拷贝(deepcopy)。

特殊情况:Python 中的数组切片

除此之外,还有一种特殊情况也被我碰到了(在赋值时使用切片操作):

original_str = [1, 2, 3]
copied_str = original_str[:]
copied_str[0] = 3
print(copied_str)  # 输出: [3, 2, 3]
print(original_str)  # 输出: [1, 2, 3]

original_str = [1, 2, 3]
copied_str = original_str
copied_str[0] = 3
print(copied_str)  # 输出: [3, 2, 3]
print(original_str)  # 输出: [3, 2, 3]

上面用到original_str[:] 是 Python 切片(slice)操作,它返回原对象的一个浅拷贝

示例

列表示例
original_list = [1, 2, 3, 4]
copied_list = original_list[:]

# 修改 copied_list 不会影响 original_list
copied_list[0] = 99

print(original_list)  # 输出: [1, 2, 3, 4]
print(copied_list)    # 输出: [99, 2, 3, 4]

在这个例子中,copied_listoriginal_list 的一个浅拷贝。对 copied_list 的修改不会影响 original_list,因为它们是两个独立的对象。

字符串示例
original_str = "hello"
copied_str = original_str[:]

print(copied_str)  # 输出: "hello"
print(copied_str is original_str)  # 输出: True

在这个字符串示例中,由于字符串是不可变对象,copied_stroriginal_str 实际上是同一个对象的引用,因此 copied_str is original_strTrue。尽管如此,copied_str = original_str[:] 仍然遵循了创建浅拷贝的语法,只是在这种情况下,它们指向了同一个对象。

注意

  • 对于可变对象(如列表),浅拷贝意味着新对象和原对象共享内部的子对象。例如,如果列表中包含其他列表,使用 message[:] 创建的新列表和原列表中的子列表仍然是共享的。
original_list = [[1, 2], [3, 4]]
copied_list = original_list[:]
copied_list[0][0] = 99

print(original_list)  # 输出: [[99, 2], [3, 4]]
print(copied_list)    # 输出: [[99, 2], [3, 4]]

在这个例子中,original_listcopied_list 都引用了相同的内部列表,所以修改其中一个的子列表内容会影响另一个。

总结

  • 数组切分创建了原对象的浅拷贝。它复制了对象的元素,但不会影响原对象。
  • 对于可变对象,这种方式非常有用,因为它可以在不改变原对象的情况下操作副本。
  • 对于不可变对象,这种操作没有实际区别,但仍然可以用于创建一个新的引用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值