373-Python中的深浅拷贝

Python中的深浅拷贝

复习几个概念

  • 变量:是一个系统表的元素,拥有指向对象的连接空间
  • 对象:被分配的一块内存,存储其所代表的值
  • 引用:是自动形成的从变量到对象的指针
  • 类型:属于对象,而非变量
  • 不可变对象:一旦创建就不可修改的对象,包括数值类型字符串布尔类型元组

(该对象所指向的内存中的值不能被改变。当改变某个变量时候,由于其所指的值不能被改变,相当于把原来的值复制一份后再改变,这会开辟一个新的地址,变量再指向这个新的地址。)

  • 可变对象:可以修改的对象,包括列表字典集合

(该对象所指向的内存中的值可以被改变。变量(准确的说是引用)改变后,实际上是其所指的值直接发生改变,并没有发生复制行为,也没有开辟新的地址,通俗点说就是原地改变。)

0.赋值

赋值: 只是复制了新对象的引用,不会开辟新的内存空间。

并不会产生一个独立的对象单独存在,只是将原有的数据块打上一个新标签,所以当其中一个标签被改变的时候,数据块就会发生变化,另一个标签也会随之改变。

1.浅拷贝

定义(只有最外层容器是新的,内部元素都是共享的)

Note

浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存(分支)。

  • 浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。
  • 如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。

浅拷贝: 创建新对象,其内容是原对象的引用。
浅拷贝之所以称为浅拷贝,是它仅仅只拷贝了一层,拷贝了最外围的对象本身,内部的元素都只是拷贝了一个引用而已。

Python浅拷贝实验

可变数据类型的浅拷贝

栗子1
import copy

a = [1,2,3]  
b = a  # 传统赋值
c = a[:]  # 切片(底层就是浅拷贝)
d = copy.copy(a)  # 浅拷贝

print(f"a的id: {id(a)}")
print(f"b的id: {id(b)}")
print(f"c的id: {id(c)}")
print(f"d的id: {id(d)}")

print(f"a[0]的id: {id(a[0])}")
print(f"b[0]的id: {id(b[0])}")
print(f"c[0]的id: {id(c[0])}")
print(f"d[0]的id: {id(d[0])}")

控制台输出:

a的id: 2596354113728
b的id: 2596354113728
c的id: 2596354069056
d的id: 2596355427712
a[0]的id: 140716802828728
b[0]的id: 140716802828728
c[0]的id: 140716802828728
d[0]的id: 140716802828728
栗子2
import copy

# 创建对象
a = [1, 2, 3]
b = [4, 5, 6]
data = [a, b]

# 传统赋值
c = data

# 浅拷贝
d = copy.copy(data)

# 验证
print(f"data的id: {id(data)}")   
print(f"c的id: {id(c)}")         # 与data相同
print(f"d的id: {id(d)}")         # 与data不同

print(f"\ndata[0]的id: {id(data[0])}")
print(f"d[0]的id: {id(d[0])}")    # 与data[0]相同

控制台输出:

data的id: 2024771407424
c的id: 2024771407424
d的id: 2024771282944
data[0]的id: 2024770027712
d[0]的id: 2024770027712

不可变数据类型的浅拷贝

栗子1
import copy

a = (1,2,3)
b = a  # 传统赋值
c = a[:]  # 切片(底层就是浅拷贝)
d = copy.copy(a)  # 浅拷贝

print(f"a的id: {id(a)}")
print(f"b的id: {id(b)}")
print(f"c的id: {id(c)}")
print(f"d的id: {id(d)}")

print(f"a[0]的id: {id(a[0])}")
print(f"b[0]的id: {id(b[0])}")
print(f"c[0]的id: {id(c[0])}")
print(f"d[0]的id: {id(d[0])}")

控制台输出:

a的id: 1958923423680
b的id: 1958923423680
c的id: 1958923423680
d的id: 1958923423680
a[0]的id: 140716802828728
b[0]的id: 140716802828728
c[0]的id: 140716802828728
d[0]的id: 140716802828728
栗子2
import copy

# 创建对象
a = (1, 2, 3)
b = (4, 5, 6)
data = (a, b)

# 传统赋值
c = data

# 浅拷贝
data_copy = copy.copy(data)

# 验证所有引用
print("验证外层元组:")
print(f"data的id: {id(data)}")         
print(f"c的id: {id(c)}")               # 与data相同
print(f"data_copy的id: {id(data_copy)}") # 与data相同

print("\n验证内部元素:")
print(f"data[0]的id: {id(data[0])}")    
print(f"c[0]的id: {id(c[0])}")          # 与data[0]相同
print(f"data_copy[0]的id: {id(data_copy[0])}") # 与data[0]相同

对于不可变类型(如元组),copy.copy()不会创建新的对象,这是因为不可变对象是安全的,不需要创建副本。
对于元组这样的不可变类型来说,浅拷贝和简单赋值的效果是完全一样的

控制条输出:

验证外层元组:
data的id: 1714974555456
c的id: 1714974555456
data_copy的id: 1714974555456

验证内部元素:
data[0]的id: 1714974379968
c[0]的id: 1714974379968
data_copy[0]的id: 1714974379968

浅拷贝有三种形式:

  1. 切片操作(底层用copy.copy实现)
  2. 工厂函数(list())
  3. copy模块中的copy函数。

浅拷贝的笔试题

a = [1, 3, 5, [7, 9]]
b = a[:]
a[1] = 2
a[3][1] = 10
print(b)  # 问b会打印出什么来

答案:[1, 3, 5, [7, 10]]

2.深拷贝

定义

深拷贝:和浅拷贝对应,深拷贝拷贝了对象的所有元素,包括多层嵌套的元素。深拷贝出来的对象是一个全新的对象,不再与原来的对象有任何关联。

Note

深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象,是“值”而不是“引用”(不是分支)

  • 拷贝第一层级的对象属性或数组元素
  • 递归拷贝所有层级的对象属性和数组元素
  • 深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。

Python深拷贝实验

可变数据类型的深拷贝

import copy

a = [1, 2, 3, [4, 5, 6]]
b = copy.deepcopy(a)

print(f"a的id: {id(a)}")
print(f"b的id: {id(b)}")

print(f"a[3]的id: {id(a[3])}")
print(f"b[3]的id: {id(b[3])}")

控制台输出:

a的id: 1467343770176
b的id: 1467345194368
a[3]的id: 1467343814848
b[3]的id: 1467345070464
结论:
  1. 对于简单的可变数据类型,深拷贝可以对对象进行完全拷贝,生成一块独立的内存空间,两个变量之间没有任何关系
  2. 对于复杂可变数据类型,深拷贝会递归拷贝所有层级的对象属性和数组元素,生成一块独立的内存空间,且两个变量之间没有任何关系

不可变数据类型的深拷贝

栗子1
import copy

a = (1, 2, 3)
b = copy.deepcopy(a)

print(f"a的id: {id(a)}")
print(f"b的id: {id(b)}")

控制台输出:

a的id: 2423245393856
b的id: 2423245393856
栗子2
import copy

a = (1, 2, 3, (4, 5, 6))
b = copy.deepcopy(a)

print(f"a的id: {id(a)}")
print(f"b的id: {id(b)}")

print(f"a[3]的id: {id(a[3])}")
print(f"b[3]的id: {id(b[3])}")

控制台输出:

a的id: 2632243691280
b的id: 2632243691280
a[3]的id: 2632243695552
b[3]的id: 2632243695552
结论:

对于不可变数据类型,深拷贝也只能拷贝对象的引用关系,结果就是指向相同内存空间

3.深浅拷贝的特殊情况

可变类型嵌套不可变类型

实验:

import copy

a = [1, 2, 3, (4, 5, 6)]
b = copy.copy(a)
c = copy.deepcopy(a)

print(f"a的id: {id(a)}")
print(f"b的id: {id(b)}")
print(f"c的id: {id(c)}")

print(f"a[3]的id: {id(a[3])}")
print(f"b[3]的id: {id(b[3])}")
print(f"c[3]的id: {id(c[3])}")

控制台输出:

a的id: 1753060524224
b的id: 1753060479552
c的id: 1753061903744
a[3]的id: 1753061609408
b[3]的id: 1753061609408
c[3]的id: 1753061609408

结论:外层可变类型可以生成内存空间完全拷贝,但是内层对象是不可变类型,只能拷贝引用关系

不可变类型嵌套可变类型⭐

实验:

import copy

a = (1, 2, 3, [4, 5, 6])
b = copy.copy(a)
c = copy.deepcopy(a)

print(f"a的id: {id(a)}")
print(f"b的id: {id(b)}")
print(f"c的id: {id(c)}")

print(f"a[3]的id: {id(a[3])}")
print(f"b[3]的id: {id(b[3])}")
print(f"c[3]的id: {id(c[3])}")

控制台输出:

a的id: 2603735950784
b的id: 2603735950784
c的id: 2603735950864
a[3]的id: 2603734778048
b[3]的id: 2603734778048
c[3]的id: 2603736157568

特殊结论:对于深拷贝,由于内层是可变类型需要拷贝,导致外层不可变类型也被拷贝,导致整体都可以进行完全拷贝

深浅拷贝总结

赋值: 值相等,地址相等
copy浅拷贝:值相等,地址不相等
deepcopy深拷贝:值相等,地址不相等