Python的秘密生活:Matryoshka陷阱
Source: Dev.to
Timothy的嵌套计划
Timothy在整理图书馆档案时哼着小曲。他感到无所不能,因为已经掌握了可以克隆列表、避免别名诅咒的 “slice” ([:]) 语法。
“Margaret,我正在重新组织图书馆的分区。我已经制作了书架的主布局图,并在移动任何东西之前做了备份。看看这个漂亮的浅拷贝。”
他展示了自己的代码,对新学到的技巧充满信心。
# Timothy's Nested Plan
shelf_A = ["Fiction", "Mystery"]
shelf_B = ["Biography", "History"]
# The Master Layout: a list of lists
library_layout = [shelf_A, shelf_B]
# The Backup: using the slice trick
backup_layout = library_layout[:] # (Surely this clones everything... right?)
# Move "Mystery" to a new location in the backup
backup_layout[0].remove("Mystery")
print(f"Backup Shelf A: {backup_layout[0]}")
print(f"Master Shelf A: {library_layout[0]}")
控制台输出
Backup Shelf A: ['Fiction']
Master Shelf A: ['Fiction']
Timothy大为震惊。原本以为 slice 能创建完全独立的列表,然而 “Mystery” 这本书也从主布局中消失了。
浅拷贝陷阱
Margaret 解释说,切片(或 list.copy())会创建一个 新的外层容器,但 共享对内部可变对象的引用。
Original List Backup List
[Box 1] [Box 2]
| |
+-------+ +-------+
| |
v v
[Inner List]
(Shared Data!)
当 Timothy 修改 backup_layout[0] 时,实际上是修改了 library_layout[0] 所指向的同一个内部列表。这就是经典的 Matryoshka(套娃)陷阱:浅拷贝只复制了第一层。
深拷贝解决方案
为了获得一个完全独立的副本——包括所有嵌套的可变对象——Timothy 需要进行 深拷贝。
import copy
shelf_A = ["Fiction", "Mystery"]
shelf_B = ["Biography", "History"]
library_layout = [shelf_A, shelf_B]
# Deep copy: recursively copies everything
backup_layout = copy.deepcopy(library_layout)
backup_layout[0].remove("Mystery")
print(f"Backup Shelf A: {backup_layout[0]}")
print(f"Master Shelf A: {library_layout[0]}")
输出
Backup Shelf A: ['Fiction']
Master Shelf A: ['Fiction', 'Mystery']
copy.deepcopy() 会遍历整个对象图,克隆它遇到的每个可变元素,因此对备份的修改永远不会影响原始对象。
性能考虑
-
为什么深拷贝不是默认?
深拷贝在时间和内存上都昂贵。对于小的或扁平的结构,开销可以忽略不计,但反复克隆大型、深度嵌套的结构会成为性能瓶颈。 -
Python 的默认哲学
Python 追求速度;浅拷贝成本低,足以满足许多使用场景。当需要对嵌套数据的安全性时,必须显式请求。
复制指南
| 情形 | 推荐的复制方式 |
|---|---|
扁平列表(例如 [1, 2, 3]) | 浅拷贝(list[:]、list.copy() 或 copy.copy(x)) |
嵌套列表/字典(例如 [[1, 2], [3, 4]]) | 深拷贝(copy.deepcopy(x)) |
| 需要对任意对象进行通用浅拷贝 | copy.copy(x)(适用于列表、字典、自定义对象) |
| 需要对复杂结构进行完全独立的克隆 | copy.deepcopy(x) |
快速参考
import copy
# 浅拷贝(仅第一层)
shallow = original_list[:] # 或 original_list.copy()
shallow_generic = copy.copy(original) # 适用于任意对象
# 深拷贝(完整递归)
deep = copy.deepcopy(original)
何时使用深拷贝
- 嵌套可变容器 – 列表的列表、包含列表的字典、对象中包含其他可变对象。
- 需要隔离 – 当对副本的修改绝不能影响原始对象,即使是在结构的深层也是如此。
- 测试或沙盒 – 为实验创建安全的沙盒版数据。
对于简单、扁平的数据结构,浅拷贝仍是最有效的选择。
故事在下一集 “The Loophole” 中继续,那里在迭代列表时修改列表会导致时间线混乱、跳过。