Python 中的赋值、浅拷贝与深拷贝:内存行为深度解析

一、核心概念

1.1 Python 的对象模型:一切皆对象,变量即引用

  • 变量不直接存储值,而是 指向对象的引用(指针)
  • id(x) 返回对象的内存地址(CPython 中即 PyObject* 的地址)。
  • 赋值操作 b = a 只是新增一个引用,不创建新对象

1.2 可变性(Mutability)

类别 典型类型 特征
不可变对象 int, float, str, tuple, frozenset, bytes 一旦创建,内容不可更改;任何“修改”操作均返回新对象
可变对象 list, dict, set, bytearray, 自定义类实例(默认) 可原地修改内容,对象身份(id)不变

⚠️ 注意:tuple 虽不可变,但若其元素为可变对象(如 ([1], 2)),则 tuple 自身不可变,但元素内容可变

二、三类操作在不同对象上的表现

2.1 不可变对象:三者无实质区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import copy
a = (1, 2, 3)

print("=====赋值=====")
b = a
print(id(a)) # 139912659462720
print(id(b)) # 139912659462720 → 同一对象

print("=====浅拷贝=====")
b = copy.copy(a)
print(id(a)) # 139912659462720
print(id(b)) # 139912659462720 → 未创建新对象

print("=====深拷贝=====")
b = copy.deepcopy(a)
print(id(a)) # 139912659462720
print(id(b)) # 139912659462720 → 仍未创建新对象

原因

  • copy.copy()copy.deepcopy() 对不可变对象 直接返回原对象(CPython 优化:避免无意义复制)。
  • 源码依据(copy.py):
1
2
3
4
def _copy_immutable(x):
return x
for t in (type(None), int, float, bool, str, tuple, ...):
dispatch[t] = _copy_immutable

✅ 结论:对不可变对象,三种操作均指向同一内存地址;试图“修改”将创建新对象(如 a + (4,))。


2.2 可变对象(单层):浅/深拷贝创建新容器,但元素仍共享

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import copy
a = [1, 2, 3]

print("=====赋值=====")
b = a
print(id(a)) # 140703391445888
print(id(b)) # 140703391445888 → 同一 list 对象

print("=====浅拷贝=====")
b = copy.copy(a)
print(id(a)) # 140703391445888
print(id(b)) # 140703391445824 → 新 list 对象
print(id(a[0])) # 140711474919728
print(id(b[0])) # 140711474919728 → 元素 1 的地址相同!

print("=====深拷贝=====")
b = copy.deepcopy(a)
print(id(a)) # 140703391445888
print(id(b)) # 140703391445696 → 新 list 对象
print(id(a[0])) # 140711474919728
print(id(b[0])) # 140711474919728 → 元素 1 的地址仍相同!

关键疑问

为何深拷贝后 id(a[0]) == id(b[0])?不是应该“完全独立”吗?

解答

  • a[0] 是整数 1 —— 不可变对象
  • deepcopy 对不可变对象 不会复制,而是直接复用(与 copy 逻辑一致)。
  • Python 对小整数(-5256)、短字符串等启用 对象池(interning),全局复用。

验证:

1
2
3
4
x = 1
a = [1, 2, 3]
b = copy.deepcopy(a)
print(id(x) == id(a[0]) == id(b[0])) # True

✅ 结论:浅/深拷贝仅对可变子对象递归复制;不可变子对象始终共享。


2.3 可变对象(嵌套):浅拷贝共享子对象,深拷贝独立

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import copy
a = [[0, 1], 2, 3] # a[0] 是可变 list

print("=====赋值=====")
b = a
print(id(a[0])) # 140087132844928
print(id(b[0])) # 140087132844928 → 同一子 list

print("=====浅拷贝=====")
b = copy.copy(a)
print(id(a[0])) # 140087132844928
print(id(b[0])) # 140087132844928 → 仍共享子 list!

print("=====深拷贝=====")
b = copy.deepcopy(a)
print(id(a[0])) # 140087132844928
print(id(b[0])) # 140087132938624 → 新子 list!

验证深拷贝独立性

1
2
3
a[0].append(999)
print(a) # [[0, 1, 999], 2, 3]
print(b) # [[0, 1], 2, 3] → 未受影响

但注意子元素地址

1
2
print(id(a[0][0]))  # 139731504490768 (整数 0)
print(id(b[0][0])) # 139731504490768 → 仍相同!

原因

  • a[0][0] 是整数 0(不可变),deepcopy 不复制不可变对象。
  • 即使深拷贝,不可变基本类型仍全局共享

✅ 结论:深拷贝的“深度”仅到可变对象边界;不可变叶子节点始终复用。


三、深度机制剖析:copy 模块如何工作?

3.1 浅拷贝(copy.copy)实现逻辑

  1. 调用对象的 __copy__ 方法(若定义);
  2. 否则,对内置可变类型:
    • listx[:]type(x)(x)
    • dictx.copy()
    • settype(x)(x)
  3. 不递归复制子对象 —— 新容器,旧元素引用。

3.2 深拷贝(copy.deepcopy)实现逻辑

  1. 维护一个 memo 字典({id(obj): copy}),防止循环引用无限递归;
  2. memo,若已拷贝则直接返回;
  3. 否则:
    • 若对象有 __deepcopy__,调用之;
    • 否则调用 __reduce_ex__ / __reduce__ 获取重建数据;
    • 对重建数据中的每个元素递归 deepcopy
  4. 仅当子对象为可变类型时,才创建新副本;不可变对象直接引用。

源码关键路径(简化):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def deepcopy(x, memo=None):
if memo is None:
memo = {}
if id(x) in memo:
return memo[id(x)]

# 不可变类型:直接返回
if type(x) in (int, str, tuple, ...):
return x

# 可变类型:递归处理
copier = _deepcopy_dispatch.get(type(x))
y = copier(x, memo)
memo[id(x)] = y
return y

四、常见误区

4.1 高频误区澄清

误区 正解
“深拷贝会复制所有对象” 仅复制可变对象;不可变对象(如 int, str, tuple)直接复用
a[:] 是深拷贝” 仅对单层 list 是浅拷贝;对嵌套结构仍是浅拷贝
dict.copy() 是深拷贝” 是浅拷贝;值若为可变对象,仍共享

4.2 正确使用场景建议

场景 推荐操作 原因
仅需避免顶层修改影响原对象 copy.copy() 或切片 [:] 开销小,满足需求
容器含嵌套可变对象,需完全隔离 copy.deepcopy() 防止子对象修改污染
仅需复制部分字段 手动构造新对象(如 new_dict = {k: v for k, v in old.items() if k != 'omit'} 更高效、意图明确
性能敏感 + 大型嵌套结构 自定义 __deepcopy__ 或使用 marshal/pickle + 优化 避免 deepcopy 的递归开销

4.3 自定义类的拷贝控制

1
2
3
4
5
6
7
8
9
10
11
class MyClass:
def __init__(self, data):
self.data = data # 可变对象

def __copy__(self):
# 浅拷贝:新实例,共享 data
return MyClass(self.data)

def __deepcopy__(self, memo):
# 深拷贝:新实例 + data 深拷贝
return MyClass(copy.deepcopy(self.data, memo))

⚠️ 注意:实现 __deepcopy__ 时务必传递 memo,否则循环引用将导致栈溢出。


五、总结

操作 是否新建容器 是否新建可变子对象 是否新建不可变子对象 典型用法
**赋值 **b = a 引用传递
**浅拷贝 **copy.copy(a) 隔离顶层修改
**深拷贝 **copy.deepcopy(a) 完全独立副本

核心原则

  • 拷贝的“深度”由子对象的可变性决定,而非操作名称。
  • 不可变对象的复用是 Python 内存优化的核心策略,理解它才能避免“地址相同=未拷贝”的误判。

附:快速验证脚本模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import copy

def check_copy_behavior(obj):
print(f"原始对象 id: {id(obj)}")

b = obj
print(f"赋值后 id: {id(b)}")

b = copy.copy(obj)
print(f"浅拷贝后 id: {id(b)}")

b = copy.deepcopy(obj)
print(f"深拷贝后 id: {id(b)}")

# 若为容器,检查首个元素
if hasattr(obj, '__getitem__') and len(obj) > 0:
print(f"元素 id (原): {id(obj[0])}")
b = copy.deepcopy(obj)
print(f"元素 id (深拷贝后): {id(b[0])}")