你提到的“从不报错”这个点,其实是Python设计哲学里最让人又爱又恨的地方。它把变量名当成贴在对象上的便利贴,而不是装东西的盒子。a = b只是多贴了一张纸,根本没动盒子里的东西。
简单说
补充一个很多人会忽略的场景:默认参数。
def append_to(item, target=[]):
target.append(item)
return target
第一次调append_to(1)返回[1],第二次调append_to(2)你以为会拿到[2],结果出来是[1, 2]。因为那个空列表在函数定义时只创建了一次,之后每次调用都在往同一个列表里塞东西。这比切片浅拷贝更隐蔽,因为它连赋值动作都没有,直接藏在函数签名里。skate_de之前好像也踩过这个坑,排查了半天才定位到。
关于你说的deepcopy能根治,技术上没问题,但性能开销值得留意。如果嵌套层级深或者数据量大,deepcopy会遍历整个对象图重建一遍,时间复杂度直接拉满。我一般倾向于按需处理:如果只是要改嵌套结构里的某一层,不如只针对那一层做显式拷贝,外层保持引用。比如用列表推导式重新生成内层列表,比无脑deepcopy轻量得多。
另外geek__399之前提过一个思路挺实用:如果你的数据结构从生成就确定不会再被修改,直接用tuple代替list,frozenset代替set。不可变类型天然免疫这种“共享修改”的问题,连拷贝都省了。虽然构建的时候麻烦一点,但后续逻辑里不用时刻提防哪根线连着哪根线。
说到底,这类bug难查是因为它破坏了我们对“局部性”的直觉预期。代码看着是在操作局部变量,实际上手伸到了别处。养成习惯,只要涉及可变对象(list、dict、set)的传递和赋值,脑子里先过一遍“这是贴标签还是造新物”,能挡掉大部分暗坑。