一、一行赋值看似爽,翻车起来真叫惨
那天同事小王火急火燎地跑过来,说他代码里出了个灵异事件:明明只改了一个列表,结果其他地方的列表也跟着变了,排查了半天才发现,罪魁祸首竟然是一行看似简单的赋值代码。
咱们先看段代码感受下:
# 先试试整数赋值a = b = c = 10c = 20print(a, b, c) # 输出 10 10 20,符合预期# 再试试列表赋值x = y = z = [1, 2, 3]x.append(4)print(x, y, z) # 输出 [1,2,3,4] [1,2,3,4] [1,2,3,4],全变了!
是不是很神奇?同样的赋值方式,整数和列表的表现居然天差地别。这就是 Python 赋值里最容易踩的坑,今天咱们就把这个坑彻底填平。
二、为什么会这样?得从内存存储说起
要理解这个问题,得先明白 Python 是怎么存储数据的。咱们可以把计算机内存想象成一个个带编号的抽屉,每个抽屉里放着数据。
当你执行a = 10
时,Python 其实做了两件事:
-
在某个抽屉(比如编号 1001)里放了 10 这个数
-
给 a 贴了个标签,让它指向抽屉 1001
而a = b = c = 10
就是同时给 a、b、c 三个标签都贴上了指向抽屉 1001 的贴纸。
不可变类型:改值等于换抽屉
整数、字符串、元组这些不可变类型有个特点:抽屉里的东西不能改。当你执行c = 20
时,并不是把抽屉 1001 里的 10 改成 20,而是另外找一个抽屉(比如 1002)放 20,然后把 c 的贴纸撕下来贴到 1002 上。这时候 a 和 b 的贴纸还在 1001 上,所以它们的值没变。
可变类型:改值是在同一个抽屉里折腾
列表、字典这些可变类型就不一样了,它们的抽屉是可以打开修改的。x = y = z = [1,2,3]
让三个标签都指向同一个放着列表的抽屉(比如 2001)。当你执行x.append(4)
时,并没有换抽屉,而是直接打开 2001 号抽屉,在里面加了个 4。所以不管看 x、y 还是 z,都是从 2001 号抽屉里拿东西,自然都看到了新增的 4。
三、两类类型大对比
为了更清楚,咱们做个表格对比:
类型分类 | 包含哪些类型 | 赋值后多个变量关系 | 修改变量的效果 | 内存操作方式 |
---|---|---|---|---|
不可变类型 | 整数 (int)、字符串 (str)、元组 (tuple)、布尔值 (bool)、浮点数 (float) | 共享值的引用,但值不可变 | 修改一个变量不影响其他 | 重新创建新值,变量指向新地址 |
可变类型 | 列表 (list)、字典 (dict)、集合 (set)、自定义对象等 | 共享同一个内存地址 | 修改一个变量会影响所有变量 | 在原内存地址上直接修改内容 |
四、怎么避免可变类型的坑?用拷贝!
既然多个变量共享一个可变类型会出问题,那解决办法就是让它们各用各的。这时候就需要用到拷贝,常见的拷贝方式有这些:
1. 列表切片拷贝
# 用切片[:]创建新列表a = [1, 2, 3]b = a[:] # 切片会生成新列表a.append(4)print(a) # [1,2,3,4]print(b) # [1,2,3],不受影响
如果是一行多变量赋值,应该这样写:
x = y = z = [1,2,3][:] # 先切片生成新列表,再赋值# 或者更清晰的写法x = [1,2,3][:]y = [1,2,3][:]z = [1,2,3][:]
2. 使用 copy () 方法
列表和字典都有 copy () 方法:
# 列表的copy()a = [1,2,3]b = a.copy()a.append(4)print(b) # [1,2,3]# 字典的copy()c = {"name": "张三", "age": 18}d = c.copy()c["age"] = 19print(d) # {'name': '张三', 'age': 18}
3. 深拷贝 (deepcopy) 处理嵌套结构
如果列表或字典里还嵌套了可变类型,上面两种方法就不够了,这时候需要用深拷贝:
import copy# 嵌套列表a = [1, 2, [3, 4]]b = a.copy() # 浅拷贝只能拷贝外层a[2].append(5)print(b) # [1, 2, [3, 4, 5]],内层还是会变# 深拷贝可以完全独立c = copy.deepcopy(a)a[2].append(6)print(c) # [1, 2, [3, 4, 5]],不受影响
五、常见问题和错误
1. 误以为列表赋值后是独立的
# 错误示例a = [1,2,3]b = a # 这只是让b指向a的列表,不是创建新列表b.append(4)print(a) # [1,2,3,4],a也变了,新手常以为a不变
2. 函数参数传递时的陷阱
def add_item(lst):lst.append("new")return lstmy_list = [1,2,3]result = add_item(my_list)print(my_list) # [1,2,3, 'new'],原列表被修改了!
解决办法是在函数内部使用拷贝:
def add_item(lst):new_lst = lst.copy() # 先拷贝再操作new_lst.append("new")return new_lst
3. 循环中使用可变类型作为默认参数
# 错误示例def func(item, lst=[]): # 默认参数只初始化一次lst.append(item)return lstprint(func(1)) # [1]print(func(2)) # [1, 2],不是预期的[2]
正确写法应该是:
def func(item, lst=None):if lst is None:lst = [] # 每次调用都创建新列表lst.append(item)return lst
六、面试时可能会被问到的问题及回答
问题 1:Python 中,a = b = [1,2,3] 和 a = [1,2,3]; b = [1,2,3] 有什么区别?
回答:
第一种写法中,a 和 b 指向同一个列表对象,修改 a 会影响 b,修改 b 也会影响 a;
第二种写法中,a 和 b 分别指向两个内容相同但内存地址不同的列表对象,它们是独立的,修改其中一个不会影响另一个。
问题 2:如何让多个变量分别持有相同内容的列表,但又互不影响?
回答:
可以使用拷贝的方式,比如切片a = [1,2,3][:]
、b = [1,2,3].copy()
,或者使用copy
模块的copy()
方法。如果列表有嵌套结构,需要用copy.deepcopy()
来确保完全独立。
问题 3:解释一下 Python 中的可变类型和不可变类型,以及它们在赋值时的区别?
回答:
可变类型是指对象创建后可以修改其内容,比如列表、字典,多个变量赋值时会共享同一个对象,修改一个会影响其他;
不可变类型是指对象创建后不能修改其内容,比如整数、字符串,多个变量赋值时虽然也共享引用,但修改时会创建新对象,不会影响其他变量。
问题 4:下面的代码输出什么?为什么?
a = [1, 2, 3]b = aa = [4, 5, 6]print(b)
回答:
输出[1, 2, 3]
。因为b = a
时 b 指向原来的列表,而a = [4,5,6]
是让 a 指向了新的列表,并没有修改原来的列表,所以 b 的值不变。这里要注意,重新赋值(=)和修改内容(如 append)是不同的操作。
七、总结
Python 的赋值陷阱看起来简单,实则考验对数据类型本质的理解。记住一句话:不可变类型随便共享,可变类型谨慎共享。
以后写代码时,只要涉及到列表、字典这些可变类型的赋值,多问自己一句:“我需要它们共享同一个对象吗?” 如果答案是否定的,那就用拷贝。
掌握了这个知识点,能帮你避免 90% 以上因 “共享引用” 导致的隐性 bug,让代码更健壮。下次再看到a = b = c = ...
这种写法,就不会掉坑里了!