Python的秘密生活:幻影副本
Source: Dev.to
引言
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}")
输出
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}")
输出
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 开发者都会以这种方式吃亏。总好过在生产数据库上出错,而是在测试脚本里学到这课。”