作为Python学习者,你是否遇到过修改一个列表,另一个列表也跟着变的"灵异事件"?这背后其实是Python的引用机制和拷贝原理在起作用。今天我们就从底层原理出发,结合代码实例,彻底搞懂Python中的深拷贝与浅拷贝,让你在实际开发中不再踩坑。

一、先搞懂:Python的赋值到底做了什么

在讲拷贝之前,我们必须先理解Python中最基础的赋值操作。很多人误以为赋值是"复制一个新对象",但实际上完全不是这样。

1.1 赋值的本质:引用传递

# 定义一个嵌套列表
a = [1, 2, 3, [1, 2, 3, [5, 6]]]
# 执行赋值操作
b = a

# 查看两个变量的内存地址
print(id(a))  # 输出:140709324567816
print(id(b))  # 输出:140709324567816

代码解释

  • b = a 并没有创建一个新的列表对象
  • 它只是让变量 b 指向了变量 a 所指向的同一个内存地址
  • 此时 ab 就像两个不同的门牌号,指向了同一间房子

1.2 赋值操作的后果

# 修改a中的元素
a[0] = 100
print(b[0])  # 输出:100

# 修改b中的嵌套列表
b[3][0] = 99
print(a[3][0])  # 输出:99

代码解释

  • 无论修改 a 还是 b,另一个变量都会受到影响
  • 因为它们本质上操作的是同一个对象
  • 这就是为什么我们需要"拷贝"——当我们想要两个独立的对象时,赋值操作就不够用了

二、浅拷贝(Shallow Copy):只复制第一层

浅拷贝是Python中最常用的拷贝方式,它会创建一个新的容器对象,但对于容器内部的元素,仍然使用原来的引用。

2.1 浅拷贝的实现方式

Python中实现浅拷贝有三种常用方法:

  1. 列表的 copy() 方法(推荐)
  2. 切片操作 [:]
  3. copy 模块的 copy() 函数
import copy

a = [1, 2, 3, [1, 2, 3, [5, 6]]]

# 三种浅拷贝方式
c1 = a.copy()       # 方法1:列表自带的copy方法
c2 = a[:]           # 方法2:切片操作
c3 = copy.copy(a)   # 方法3:copy模块的copy函数

# 验证:新对象与原对象地址不同
print(id(a) == id(c1))  # 输出:False

代码解释

  • 三种方式都创建了一个新的列表对象
  • 所以 ac1 的内存地址不同
  • 这是浅拷贝与赋值操作最本质的区别

2.2 浅拷贝的核心特点:只拷贝第一层

这是浅拷贝最关键也是最容易出错的地方。我们来看一个例子:

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

# 修改第一层元素
c[1] = 0
print("a:", a)  # 输出:a: [1, 2, 3, [1, 2, 3, [5, 6]]]
print("c:", c)  # 输出:c: [1, 0, 3, [1, 2, 3, [5, 6]]]

代码解释

  • 修改 c 的第一层元素(整数 2 改为 0
  • 原列表 a 没有受到任何影响
  • 因为第一层元素被成功拷贝了,现在是独立的
# 修改嵌套的第二层元素
c[3][0] = 9
print("a:", a)  # 输出:a: [1, 2, 3, [9, 2, 3, [5, 6]]]
print("c:", c)  # 输出:c: [1, 0, 3, [9, 2, 3, [5, 6]]]

代码解释

  • 修改 c 中嵌套列表的元素(1 改为 9
  • 原列表 a 中的嵌套列表也跟着变了!
  • 这就是浅拷贝的局限性:只拷贝了最外层的容器,内部的嵌套对象仍然是共享引用

2.3 浅拷贝的内存模型

原列表a: [1, 2, 3, 引用A]
                    ↓
                  [1, 2, 3, 引用B]
                                  ↓
                                [5, 6]

浅拷贝后的c: [1, 2, 3, 引用A]  # 注意:引用A和a中的引用A指向同一个对象

三、深拷贝(Deep Copy):递归复制所有层级

当我们需要完全独立的对象,无论嵌套多少层都互不影响时,就需要使用深拷贝。

3.1 深拷贝的实现方式

深拷贝只能通过Python内置的 copy 模块的 deepcopy() 函数实现:

import copy

a = [1, 2, 3, [1, 2, 3, [5, 6]]]
# 执行深拷贝
d = copy.deepcopy(a)

# 验证:新对象与原对象地址不同
print(id(a) == id(d))  # 输出:False

3.2 深拷贝的核心特点:完全独立

深拷贝会递归地复制所有层级的对象,创建一个完全独立的副本。无论修改哪一层,原对象都不会受到影响。

# 修改第一层元素
d[1] = 0
print("a:", a)  # 输出:a: [1, 2, 3, [1, 2, 3, [5, 6]]]
print("d:", d)  # 输出:d: [1, 0, 3, [1, 2, 3, [5, 6]]]

# 修改第二层元素
d[3][0] = 9
print("a:", a)  # 输出:a: [1, 2, 3, [1, 2, 3, [5, 6]]]
print("d:", d)  # 输出:d: [1, 0, 3, [9, 2, 3, [5, 6]]]

# 修改第三层元素
d[3][3][0] = 100
print("a:", a)  # 输出:a: [1, 2, 3, [1, 2, 3, [5, 6]]]
print("d:", d)  # 输出:d: [1, 0, 3, [9, 2, 3, [100, 6]]]

代码解释

  • 无论修改 d 的哪一层元素,原列表 a 都完全不受影响
  • 因为深拷贝递归地复制了所有层级的对象
  • 现在 d 是一个完全独立的对象,与 a 没有任何共享引用

3.3 深拷贝的内存模型

原列表a: [1, 2, 3, 引用A]
                    ↓
                  [1, 2, 3, 引用B]
                                  ↓
                                [5, 6]

深拷贝后的d: [1, 2, 3, 引用A']  # 引用A'指向一个新的列表对象
                     ↓
                   [1, 2, 3, 引用B']  # 引用B'指向另一个新的列表对象
                                   ↓
                                 [5, 6]  # 这也是一个新的列表对象

四、浅拷贝 vs 深拷贝:核心对比总结

特性 赋值操作 浅拷贝 深拷贝
创建新对象 ❌ 否 ✅ 是(仅外层) ✅ 是(所有层级)
第一层修改影响原对象 ✅ 是 ❌ 否 ❌ 否
嵌套层修改影响原对象 ✅ 是 ✅ 是 ❌ 否
性能 最快 较快 最慢(递归复制)
内存占用 最少 较少 最多
适用场景 不需要独立对象 无嵌套或仅一层嵌套 多层嵌套且需要完全独立

五、重要注意事项与常见误区

5.1 不可变类型的拷贝问题

# 不可变类型(如整数、字符串、元组)没有copy属性
a = (1, 2, 3)
a.copy()  # 报错:AttributeError: 'tuple' object has no attribute 'copy'

# 对不可变类型执行浅拷贝,实际上返回的是原对象的引用
import copy
b = copy.copy(a)
print(id(a) == id(b))  # 输出:True

解释

  • 不可变类型一旦创建就不能修改,所以拷贝它们没有意义
  • Python会直接返回原对象的引用,以节省内存和提高性能

5.2 循环引用的处理

深拷贝能够正确处理循环引用的情况,不会导致无限递归:

import copy

# 创建一个循环引用
a = [1, 2]
a.append(a)

# 深拷贝仍然可以正常工作
b = copy.deepcopy(a)
print(b)  # 输出:[1, 2, [...]]

5.3 自定义对象的拷贝

对于自定义类的对象,浅拷贝和深拷贝同样适用:

import copy

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

# 创建一个包含自定义对象的列表
p1 = Person("张三", 20)
a = [p1, 100]

# 浅拷贝
c = copy.copy(a)
c[0].age = 30
print(p1.age)  # 输出:30(浅拷贝,自定义对象仍然共享引用)

# 深拷贝
d = copy.deepcopy(a)
d[0].age = 40
print(p1.age)  # 输出:30(深拷贝,自定义对象也被复制了)

六、实际应用场景

6.1 什么时候用浅拷贝

  • 当你需要一个新的容器,但容器内部的元素不需要修改时
  • 当容器只有一层,没有嵌套结构时
  • 当你关心性能,且不需要完全独立的对象时

示例

# 遍历列表并修改部分元素,不影响原列表
original = [1, 2, 3, 4, 5]
new_list = original.copy()

for i in range(len(new_list)):
    if new_list[i] % 2 == 0:
        new_list[i] *= 2

print(original)  # 输出:[1, 2, 3, 4, 5]
print(new_list)  # 输出:[1, 4, 3, 8, 5]

6.2 什么时候必须用深拷贝

  • 当容器有多层嵌套结构时
  • 当你需要完全独立的副本,修改任何部分都不影响原对象时
  • 当容器内部包含可变对象(如列表、字典、自定义对象)时

示例

# 保存游戏状态的快照
game_state = {
    "player": {"name": "小明", "hp": 100, "position": [10, 20]},
    "enemies": [{"name": "怪物1", "hp": 50}, {"name": "怪物2", "hp": 80}]
}

# 保存当前状态(必须用深拷贝)
saved_state = copy.deepcopy(game_state)

# 继续游戏,修改状态
game_state["player"]["hp"] = 50
game_state["player"]["position"][0] = 15

# 读取存档
game_state = saved_state
print(game_state["player"]["hp"])  # 输出:100
print(game_state["player"]["position"])  # 输出:[10, 20]

七、扩展知识点:其他拷贝相关技巧

7.1 字典的拷贝

字典也有自己的 copy() 方法,同样是浅拷贝:

original_dict = {"a": 1, "b": [2, 3]}
shallow_copy = original_dict.copy()

shallow_copy["b"][0] = 99
print(original_dict["b"])  # 输出:[99, 3](浅拷贝,嵌套列表共享引用)

# 深拷贝字典
deep_copy = copy.deepcopy(original_dict)
deep_copy["b"][1] = 100
print(original_dict["b"])  # 输出:[99, 3]

7.2 列表推导式与拷贝

列表推导式也可以实现浅拷贝:

original = [1, 2, 3, [4, 5]]
shallow_copy = [x for x in original]

shallow_copy[3][0] = 99
print(original[3])  # 输出:[99, 5]

7.3 性能对比

深拷贝比浅拷贝慢得多,尤其是对于大的嵌套结构:

import timeit
import copy

# 创建一个大的嵌套列表
big_list = [[i for i in range(100)] for j in range(100)]

# 测试浅拷贝性能
shallow_time = timeit.timeit(lambda: big_list.copy(), number=1000)

# 测试深拷贝性能
deep_time = timeit.timeit(lambda: copy.deepcopy(big_list), number=1000)

print(f"浅拷贝耗时: {shallow_time:.4f}秒")
print(f"深拷贝耗时: {deep_time:.4f}秒")
print(f"深拷贝比浅拷贝慢 {deep_time/shallow_time:.1f} 倍")

典型输出

浅拷贝耗时: 0.0021秒
深拷贝耗时: 0.3456秒
深拷贝比浅拷贝慢 164.6 倍

八、总结

  1. 赋值操作:只是创建一个新的引用,指向同一个对象,修改任何一个都会影响另一个
  2. 浅拷贝:创建一个新的容器对象,但内部元素仍然共享引用,只适用于无嵌套或单层嵌套的情况
  3. 深拷贝:递归复制所有层级的对象,创建完全独立的副本,适用于多层嵌套的情况
  4. 性能考虑:浅拷贝速度快、内存占用少;深拷贝速度慢、内存占用多
  5. 不可变类型:不需要拷贝,Python会直接返回原对象的引用

最佳实践

  • 优先使用浅拷贝,只有在必须完全独立时才使用深拷贝
  • 对于简单的单层结构,使用列表的 copy() 方法或切片操作 [:]
  • 对于多层嵌套结构,使用 copy.deepcopy()
  • 注意循环引用和自定义对象的拷贝行为

理解了深拷贝与浅拷贝的本质,你就再也不会遇到"修改一个列表,另一个列表也跟着变"的困惑了。在实际开发中,根据具体需求选择合适的拷贝方式,既能保证代码的正确性,又能提高程序的性能。

更多推荐