python中赋值、浅拷贝和深拷贝的区别

python中赋值、浅拷贝和深拷贝的区别

一、赋值

在 Python 中,对象的赋值就是简单的对象引用,这点和 C++不同,如下所示:

a = [1,2,"hello",['python', 'C++']]
b = a

在上述情况下,a 和 b 是一样的,他们指向同一片内存,b 不过是 a 的别名,是引用。

我们可以使用bisa去判断,返回 True,表明他们地址相同,内容相同,也可以使用 id()函数来查看两个列表的地址是否相同。
赋值操作(包括对象作为参数、返回值)不会开辟新的内存空间,它只是复制了对象的引用。也就是说除了 b 这个名字之外,没有其他的内存开销。
修改了 a,也就影响了 b,同理,修改了 b,也就影响了 a
这一点仅限于可变类型,不可变类型不会生效,如下:

>>> a = [1,2,"hello",['python', 'C++']]
>>> b=a
>>> print(a)
[1, 2, 'hello', ['python', 'C++']]
>>> print(b)
[1, 2, 'hello', ['python', 'C++']]
>>> a.append('4')
>>> print(a)
[1, 2, 'hello', ['python', 'C++'], '4']
>>> print(b)
[1, 2, 'hello', ['python', 'C++'], '4']
>>> b.append('5')
>>> print(b)
[1, 2, 'hello', ['python', 'C++'], '4', '5']
>>> print(a)
[1, 2, 'hello', ['python', 'C++'], '4', '5']
>>> 
>>> 
>>> 
>>> c=8
>>> d=c
>>> print(c)
8
>>> print(d)
8
>>> d=19
>>> print(d)
19
>>> print(c)
8
>>> c=30
>>> print(c)
30
>>> print(d)
19

二、浅拷贝(shallow copy)

浅拷贝会创建新对象,其内容非原对象本身的引用,而是原对象内第一层对象的引用。
浅拷贝有三种形式:

  • 切片操作
  • 工厂函数
  • copy 模块中的 copy 函数

比如上述的列表 a;

# 切片操作:
b = a[:] 或者 b = [x for x in a];

# 工厂函数:
b = list(a);

# copy 函数:
b = copy.copy(a);

浅拷贝产生的列表 b 不再是列表 a 了,使用 is 判断可以发现他们不是同一个对象,使用 id 查看,他们也不指向同一片内存空间。但是当我们使用 id(x) for x in aid(x) for x in b 来查看 a 和 b 中元素的地址时,可以看到二者包含的元素的地址是相同的。在这种情况下,列表 a 和 b 是不同的对象,修改列表 b 理论上不会影响到列表 a。

但是要注意的是,浅拷贝之所以称之为浅拷贝,是它仅仅只拷贝了一层,在列表 a 中有一个嵌套的list,如果我们修改了它,情况就不一样了。

2.1、非嵌套对象的浅拷贝

对于非嵌套对象(即对象中不包含其他复杂对象的情况),浅拷贝和深拷贝行为类似,因为没有子对象需要特别处理。例如,对于一个整数列表:

import copy

original_list = [1, 2, 3]

# 浅拷贝
shallow_copied_list = copy.copy(original_list)

# 修改拷贝后的列表
shallow_copied_list[0] = 100

print("Original List:", original_list)
print("Shallow Copied List:", shallow_copied_list)
print("Original List id:", id(original_list))
print("Shallow Copied List id:", id(shallow_copied_list))

#输出将会是:
Original List: [1, 2, 3]
Shallow Copied List: [100, 2, 3]
'Original List id:', 4364476320
'Shallow Copied List id:', 4364584512

在这个非嵌套的例子中,修改拷贝后的列表不会影响原列表,因为整数是不可变类型,浅拷贝在这里的效果与深拷贝相同。

2.2、嵌套对象的浅拷贝

当对象中包含其他可变类型的对象(如列表、字典等)时,浅拷贝的行为就会有所不同。考虑一个包含列表的列表:

import copy

original_nested_list = [[1, 2], [3, 4]]

# 浅拷贝
shallow_copied_nested_list = copy.copy(original_nested_list)

# 修改拷贝后的嵌套列表的第一个子列表
shallow_copied_nested_list[0][0] = 100

print("Original Nested List:", original_nested_list)
print("Shallow Copied Nested List:", shallow_copied_nested_list)
print("Original Nested List id:", id(original_nested_list))
print("Shallow Copied Nested List id:", id(shallow_copied_nested_list))
print("Original Inner List 1's id:", id(original_nested_list[0]))
print("Copied Inner List 1's id:", id(shallow_copied_nested_list[0]))

#输出将是:
Original Nested List: [[100, 2], [3, 4]]
Shallow Copied Nested List: [[100, 2], [3, 4]]
'Original Nested List id:', 4364584272
'Shallow Copied Nested List id:', 4364584912
"Original Inner List 1's id:", 4364584352
"Copied Inner List 1's id:", 4364584352

在这个嵌套对象的例子中,尽管我们对shallow_copied_nested_list进行了修改,但因为浅拷贝仅仅复制了外层列表的引用,内部子列表仍然是原始对象和拷贝对象**共享**的。因此,修改子列表会影响原始对象。

总结来说,浅拷贝在处理非嵌套的简单对象时,其效果接近于深拷贝,但在处理嵌套对象时,仅复制外层结构,内部的可变子对象仍保持引用关系,从而可能导致意料之外的数据同步修改。

三、深拷贝(deep copy)

深拷贝只有一种形式,copy 模块中的 deepcopy()函数。
深拷贝和浅拷贝对应,深拷贝拷贝了对象的所有元素,包括多层嵌套的元素。因此,它的时间和空间开销要高。同样的对列表 a,如果使用 b = copy.deepcopy(a),再修改列表 b 将不会影响到列表 a,即使嵌套的列表具有更深的层次,也不会产生任何影响,因为深拷贝拷贝出来的对象根本就是一个全新的对象,不再与原来的对象有任何的关联。下面将举例说明:

3.1、非嵌套对象的深拷贝

对于非嵌套对象(如简单的列表、字符串等),深拷贝和浅拷贝的行为在结果上看起来相同,都会创建一个内容相同的新对象,但即使这种情况下,深拷贝也会为顶层对象分配新的内存空间。不过,由于非嵌套对象不包含其他复杂对象,深拷贝的额外价值不大。下面是一个简单的例子:

import copy

# 非嵌套对象示例:简单列表
original_list = [1, 2, 3]
deep_copied_list = copy.deepcopy(original_list)

deep_copied_list[0] = 300

print("Original List:", original_list)
print("Deep Copied List:", deep_copied_list)
print("Original List's id:", id(original_list))
print("Deep Copied List's id:", id(deep_copied_list))

# 输出将是:
'Original List:', [1, 2, 3]
'Deep Copied List:', [300, 2, 3]
"Original List's id:", 4305010592
"Deep Copied List's id:", 4305010512

尽管两者的id()不同,但因为是非嵌套对象,浅拷贝在此情况下也会达到同样的效果。

3.2、嵌套对象的深拷贝

深拷贝在处理嵌套对象时的作用尤为显著,因为它会创建所有层级的新副本,包括内部的可变对象。这意味着对拷贝对象的任何修改都不会影响到原对象。以下是一个嵌套列表的例子:

# 嵌套对象示例:列表中的列表
original_nested_list = [[1, 2], [3, 4]]
deep_copied_nested_list = copy.deepcopy(original_nested_list)

print("Original Nested List's id:", id(original_nested_list))
print("Deep Copied Nested List's id:", id(deep_copied_nested_list))
print("Original Inner List 1's id:", id(original_nested_list[0]))
print("Deep Copied Inner List 1's id:", id(deep_copied_nested_list[0]))

# 修改拷贝后的内部列表
deep_copied_nested_list[0][0] = 100

print("After modification:")
print("Original Nested List:", original_nested_list)
print("Deep Copied Nested List:", deep_copied_nested_list)
输出可能如下:

Original Nested List's id: 140568768768800
Deep Copied Nested List's id: 140568768769088
Original Inner List 1's id: 140568768769600
Deep Copied Inner List 1's id: 140568768770368
After modification:
Original Nested List: [[1, 2], [3, 4]]
Deep Copied Nested List: [[100, 2], [3, 4]]

在这个例子中,不仅外层列表的id()不同,内部子列表的id()也不同,证明了深拷贝为所有层级的对象都创建了全新的副本。因此,对深拷贝后的内部列表做修改,不会影响到原始的嵌套列表。

四、拷贝的注意点

对于非容器类型,如数字、字符,以及其他的“原子”类型,没有拷贝一说,产生的都是原对象的引用。
如果元组变量值包含原子类型对象,即使采用了深拷贝,也只能得到浅拷贝。

五、相关知识

5.1、python 可变类型与不可变类型

Python中的数据类型主要分为可变类型(mutable)和不可变类型(immutable)两大类:

不可变类型(Immutable Types)
  • 整型(int):一旦创建,其值就不能改变。
  • 浮点型(float):同整型,值不可变。
  • 字符串(str):字符串的每个字符都不能被单独修改。
  • 元组(tuple):一旦创建,其中的元素就不能被修改。
  • 数字(numbers模块中的类型,如complex):数值类型一般被认为是不可变的。
  • 布尔型(bool):True和False,固定值,不可变。
可变类型(Mutable Types)
  • 列表(list):可以添加、删除、修改元素。
  • 字典(dict):可以修改、添加、删除键值对。
  • 集合(set):可以添加、删除元素。
  • 字节序列(bytearray):与bytes相似,但可以修改。
  • 自定义类的实例(除非明确设计为不可变):默认情况下,实例的属性可以被修改。

5.2、python可变类型与c 、golang 等语言指针类型的比较

在C语言中,指针直接存储变量的内存地址,允许直接操作内存中的数据。这使得开发者可以直接修改数据结构的内容,无论是基本类型还是复杂结构,都可以通过指针来改变。C语言中没有“不可变”这一概念的直接支持,任何数据都可以通过指针变为“可变”。

Go语言中也有指针,通过在类型前加上*来定义指针类型。Go中的指针传递与C语言相似,允许函数修改传递的值(如果是指针或引用类型)。Go语言中的数组、切片、map、channel等类型在一定程度上体现了可变与不可变的特性,但这些类型不是基于指针的概念直接定义的“可变性”。

关系简述:

Python中的不可变类型与指针无关,因为它们不允许修改值,这与C或Go中通过指针修改数据形成对比。
Python的可变类型虽然在功能上可能类似于通过指针修改数据,但实际上Python的内部机制并不直接暴露指针操作,而是通过高级数据结构实现。
C和Go中的指针提供了底层的内存访问能力,允许直接修改数据,这种控制力在Python中是通过数据类型的可变性间接提供的,而不是直接操作指针。
Python的变量更像是标签,它们指向对象,对于不可变类型,重新赋值只是让变量指向了新的对象,而对于可变类型,可以修改对象的内容而不改变其引用。这一点与C或Go中通过指针操作内存有本质不同。

参考文档

1、https://www.cnblogs.com/big-devil/p/7625898.html
2、https://cloud.tencent.com/developer/article/2266470

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值