
Python 深浅拷贝陷阱:改一个列表元素,另一个列表也跟着变的 bug 怎么破你有没有遇到过这种诡异现象:复制了一份列表,只改了副本里的一个元素,结果原始列表也被改了?或者用[[0]*3]*3建了个二维数组,给matrix[0][0]赋值,结果整整一列全变了?这不是 Python 的 bug,是浅拷贝共享了内部引用。搞不清、copy()、deepcopy()三者的区别,迟早会写出这种「改一个动全身」的隐蔽 bug。这篇把 Python 拷贝的三个层级讲透,用能跑的例子看清每一层到底复制了什么。先搞清:赋值根本不是拷贝很多人以为b a是复制了一份,其实完全没有。只是给同一个对象多起了个名字,两个变量指向内存里同一个对象。a[1,2,3]ba# b 和 a 是同一个对象的两个名字b.append(4)print(a)# [1, 2, 3, 4] ← a 也变了!print(aisb)# True ← 同一个对象a is b为 True 就是铁证:它们是同一个对象。想验证两个变量是不是「同一个东西」,用is(比较身份/内存地址),不是(比较值)。所以只要你想「改副本不影响原件」,就绝不能用,得真正拷贝一份。浅拷贝:复制最外层,但内部对象还是共享的浅拷贝(shallow copy)会新建一个外层容器,但里面的元素还是原来那些对象的引用。对一维列表够用,对嵌套结构就出事。几种等价的浅拷贝写法:importcopy a[1,2,3]b1a.copy()# 列表自带的 copy 方法b2a[:]# 切片b3list(a)# 构造函数b4copy.copy(a)# copy 模块# 以上四种都是浅拷贝,效果一样对一维(元素是不可变的数字/字符串)来说,浅拷贝就够了,改副本不影响原件:a[1,2,3]ba.copy()b[0]99print(a)# [1, 2, 3] ← a 没变,安全但只要列表里嵌套了可变对象(列表、字典),浅拷贝的问题就暴露了:a[[1,2],[3,4]]ba.copy()# 浅拷贝b[0][0]99# 改的是内层列表的元素print(a)# [[99, 2], [3, 4]] ← a 的内层也变了!print(a[0]isb[0])# True ← 内层是同一个列表对象外层b确实是新列表(a is b为 False),但b[0]和a[0]指向的是同一个内层列表。改b[0][0]等于改那个共享的内层列表,a自然跟着变。这就是「改副本却动了原件」bug 的根源:浅拷贝只复制了一层皮,里面还是连着的。深拷贝:递归复制每一层,彻底断开深拷贝(deep copy)会递归地把每一层嵌套对象都复制一份,新旧对象完全独立,改哪个都不影响另一个。importcopy a[[1,2],[3,4]]bcopy.deepcopy(a)# 深拷贝b[0][0]99print(a)# [[1, 2], [3, 4]] ← a 纹丝不动print(a[0]isb[0])# False ← 内层也是各自独立的对象需要「完全独立的副本、改动互不影响」时,嵌套结构一律用copy.deepcopy()。代价是它要递归复制所有层,数据量大时比浅拷贝慢、也更费内存,所以别无脑到处 deepcopy。一张对照表记住三者:操作外层容器内层可变对象b a共享(同一个)共享a.copy()/a[:]新建共享(坑!)copy.deepcopy(a)新建新建(独立)经典翻车现场:[[0]*3]*3建二维数组这是初学者最常踩的坑,本质也是浅拷贝(引用复制)。想建一个 3×3 的二维列表,很多人写:matrix[[0]*3]*3# 看起来对,其实是陷阱matrix[0][0]1print(matrix)# [[1, 0, 0], [1, 0, 0], [1, 0, 0]] ← 三行都变了!为什么一改全变?[x] * 3是把同一个内层列表的引用重复了三次,三行其实是同一个列表对象:print(matrix[0]ismatrix[1])# True ← 三行是同一个对象正确写法:用列表推导式,每次循环都新建一个内层列表:matrix[[0]*3for_inrange(3)]# 每行是独立的新列表matrix[0][0]1print(matrix)# [[1, 0, 0], [0, 0, 0], [0, 0, 0]] ← 只改了一个,正确print(matrix[0]ismatrix[1])# False ← 各自独立记住:要「N 个独立的可变对象」,永远用推导式[... for _ in range(n)],不要用[obj] * n。后者只在元素不可变(数字、字符串)时才安全。字典同理,还有个 dict 默认值的坑字典的.copy()也是浅拷贝,嵌套字典照样共享:config{db:{host:localhost}}backupconfig.copy()# 浅拷贝backup[db][host]remoteprint(config[db][host])# remote ← 原字典也变了需要独立就copy.deepcopy(config)。另一个相关陷阱是dict.fromkeys配可变默认值:ddict.fromkeys([a,b],[])# 想给每个 key 一个空列表d[a].append(1)print(d)# {a: [1], b: [1]} ← b 也有 1,共享了同一个列表!同样是「一个对象被多个 key 共享」。要每个 key 独立的可变默认值,用推导式或defaultdict(list):d{k:[]forkin[a,b]}# 每个 key 独立的列表怎么自查:用 is 而不是 排查这类 bug 的通用手段:怀疑两个东西「是不是同一个对象」时,用is打印验证。只告诉你值相不相等,is才告诉你是不是同一块内存。a[[1,2]]ba.copy()print(ab)# True 值相等print(aisb)# False 外层是两个对象print(a[0]isb[0])# True 但内层共享 ← 这才是 bug 源头看到内层is为 True,就知道该上deepcopy了。小结Python 的拷贝分三层,搞混就会写出「改一个动全身」的 bug:赋值:不拷贝,只是同一对象的别名,a is b为 True → 想要副本绝不能用它。浅拷贝(.copy()/[:]/list()):只新建外层,内层可变对象仍共享 → 一维安全,嵌套结构会翻车。深拷贝(copy.deepcopy):递归复制每一层,彻底独立 → 嵌套结构要「改动互不影响」时用,但更慢更费内存。[obj]*n陷阱:重复的是同一个引用 → 建 N 个独立可变对象一律用推导式[... for _ in range(n)]。自查工具:用is(比身份)而不是(比值),内层is为 True 就该 deepcopy 了。一句话记忆点:浅拷贝只复制一层皮,里面还连着;嵌套结构要真正断开,只有 deepcopy 和推导式。