Python的秘密生活:幻影副本
Source: Dev.to
为什么在 Python 中 = 并不会真正复制你的数据。
Timothy 盯着屏幕,脸色苍白。“Margaret?我好像不小心删掉了一半的数据库。”
Margaret 立刻把椅子转过去,语气平静。“别慌。把发生的事情说清楚。”
“我在测试一个清理用户列表的脚本,” Timothy 解释道。“我想安全地测试它,所以先复制了一份列表。我以为如果我把复制的列表弄坏,原始的列表还能安全。”
他把代码给她看:
# Timothy's Safety Plan
# The original list of critical users
users = ["Alice", "Bob", "Charlie", "Dave"]
# Create a "backup" copy to test on
test_group = users
# Timothy deletes 'Alice' from the test group
test_group.remove("Alice")
# Check the results
print(f"Test Group: {test_group}")
print(f"Original Users: {users}")
Output
Test Group: ['Bob', 'Charlie', 'Dave']
Original Users: ['Bob', 'Charlie', 'Dave']
Timothy 垂头丧气地说:“看见了吗?我从 test_group 删除了 Alice,但她也从 users 列表里消失了!这怎么可能?我操作的是备份,而不是原始列表!”
地址,而不是房子
Margaret 研究了一下代码。“这是 Python 中最常见的误解之一。关键在于 Python 如何处理内存。”
她问:“当你写 test_group = users 时,你以为这条指令做了什么?”
Timothy 回答:“我以为它创建了一个新列表。我以为它把 users 中的所有名字复制到一个叫 test_group 的新变量里。”
“Python 为了效率会走捷径,” Margaret 解释道。“复制数据会消耗时间和内存,所以 Python 并不是复制房子,而是只复制地址。”
她画了一个简单的示意图:变量 users 指向一个装有名字列表的盒子。当你执行 test_group = users 时,实际上是把第二个变量指向同一个地址;两个名字指向完全相同的对象。
“所以
users和test_group只是同一个对象的两个不同名字?”
“没错,” Margaret 微笑道。“这就像通过链接共享文档——任何人通过该链接看到的都是同一个文档,所做的修改对所有人都可见。”
打破链接
Timothy 问:“那我要怎么真正复制一份呢?我想要一个独立的盒子。”
“我们必须显式地说明,” Margaret 说。“告诉 Python 把数据取出来,构建一个新列表。”
她给他演示了 .copy() 方法:
# Margaret's Fix: Explicit Copying
users = ["Alice", "Bob", "Charlie", "Dave"]
# .copy() creates a brand new list with the same data
test_group = users.copy()
test_group.remove("Alice")
print(f"Test Group: {test_group}")
print(f"Original Users: {users}")
Output
Test Group: ['Bob', 'Charlie', 'Dave']
Original Users: ['Alice', 'Bob', 'Charlie', 'Dave']
Timothy 长舒了一口气。“Alice 安全了。”
Margaret 还补充了一个警告:.copy() 只会产生浅拷贝。如果列表中包含其他可变对象(例如内部列表),这些内部对象仍然是共享的。对于仅包含名字的简单列表,浅拷贝已经足够。
Margaret 的速查表
- 陷阱: 认为
new_list = old_list会创建副本。 - 现实: 赋值(
=)创建的是引用(一个昵称),而不是副本。两个变量指向同一个对象。 - 原因: Python 这样做是为了节省内存并提升速度。
解决方案
- 浅拷贝:
new_list = old_list.copy()(推荐方式)。 - 切片:
new_list = old_list[:](较早的做法,但仍然常用)。
检查方法
你可以验证两个变量是否引用同一个对象:
id(a) == id(b) # 如果 a 和 b 是同一个对象则返回 True
Timothy 在编辑器里记下笔记:“我再也不会把 = 当作‘复制’来使用了。”
“这是一种成长的仪式,” Margaret 向他保证。“每个 Python 开发者都会以艰难的方式学到这课。总好过在生产数据库上犯错,而是在测试脚本里先学会。”