什么是变量?打个比方,你桌上有杯咖啡,你在杯子上贴了张便签写着"我的"。这个便签就是变量名,咖啡就是数据。你撕下便签贴到旁边的奶茶上 ,变量名没变,但它指向的东西变了。
Python里的变量就是这么回事——它不是盒子,它是便签。
至于作用域,说白了就是这张便签在哪个范围内有效。你在会议室里贴的便签,出了会议室门就没人认了。函数里定义的变量,函数外 面也看不到。
变量和作用域的概念本身不难,难的是它们组合在一起之后会冒出来的各种坑。下面这几个bug,我基本都亲手踩过,每一个单独拿出来都能被写成面试题。
坑 1:链式赋值的引用陷阱
今天这个,是我实习时候的翻车现场,当年被老板拿来讲了好久。
a = b = []
a.append(1)
print(b)
你觉得输出是什么?想好了吗?
实际是:[1]
链式赋值 a = b = [] 让 a 和 b 指向同一个列表对象。这不是创建了两个空列表,而是创建了一个空列表然后让两个变量都引用它。对 a 的修改自然会反映到 b 上。
正确写法:
a = []
b = []
a.append(1)
print(b) # []
踩坑现场:初始化多个缓存字典用了 a = b = {},结果两个缓存互相污染,线上数据全乱了。
坑 2:字典推导式变量泄漏
Stack Overflow 上这道题被问了几万次,答案都在评论里吵架。
x = 'before'
lst = [x for x in range(3)]
print(x)
你觉得输出是什么?想好了吗?
实际是:before
在 Python 3 中,列表推导式有自己的作用域,不会泄漏变量。但在 Python 2 中,x 会被覆盖为 2。如果你的代码需要兼容 Python 2,这就是一个陷阱。
正确写法:
# Python 3 中推导式不泄漏变量
# 但 for 循环会:
for x in range(3):
pass
print(x) # 2,for 循环的变量会泄漏
踩坑现场:维护一个 Python 2/3 兼容的项目,某个函数在 Python 3 上正常但 Python 2 上出错,排查发现是推导式变量泄漏。
坑 3:循环中修改列表
教程里不写,文档里提一句就过,但这个问题能让你绕路绕很久。
nums = [1, 2, 3, 4, 5, 6]
for i, n in enumerate(nums):
if n % 2 == 0:
nums.remove(n)
print(nums)
你觉得输出是什么?想好了吗?
实际是:[1, 3, 5, 6]
在遍历列表的同时修改它,会导致迭代器跳过某些元素。当你删除索引 1 处的 2 后,原来索引 2 处的 3 变成了索引 1,但迭代器已经移到索引 2,直接跳到了 4。所以 6 被跳过了没有被检查到。
正确写法:
nums = [1, 2, 3, 4, 5, 6]
nums = [n for n in nums if n % 2 != 0]
print(nums) # [1, 3, 5]
踩坑现场:批量清理数据库查询结果中的无效记录,上线后总有零星脏数据漏掉,查了好久才定位到这个问题。
坑 4:全局变量赋值前引用
这个行为是 Python 的设计决定,但很多人到现在还以为是 bug。
报错:UnboundLocalError: local variable 'x' referenced before assignment
x = 10
def foo():
print(x)
x = 20
foo()
Python 在编译函数时,发现函数体内有 x = 20 的赋值语句,就把 x 标记为局部变量。然后执行 print(x) 时,局部变量 x 还没赋值,就报 UnboundLocalError。Python 不会先去全局找 x 再切回局部。
修复:
x = 10
def foo():
global x # 明确声明使用全局变量
print(x)
x = 20
foo()
踩坑现场:在函数里想先读取再修改一个全局配置,运行直接报错,新手最容易在这里困惑。
坑 5:闭包中的变量绑定
有个同事把这个错误带进了 release,上线前一分钟才被抓出来,全组出了一身冷汗。
funcs = []
for i in range(5):
funcs.append(lambda: i)
print([f() for f in funcs])
你觉得输出是什么?想好了吗?
实际是:[4, 4, 4, 4, 4]
lambda 捕获的是变量 i 的引用,而不是它在创建时的值。当循环结束后,i 的最终值是 4,所以所有 lambda 调用时都拿到 4。这是 Python 闭包的经典陷阱。
正确写法:
funcs = []
for i in range(5):
funcs.append(lambda i=i: i) # 默认参数绑定当前值
print([f() for f in funcs]) # [0, 1, 2, 3, 4]
踩坑现场:写一个按钮回调注册函数,5 个按钮点哪个都触发最后一个的逻辑,用户反馈说界面有 bug。
这 5 个坑不复杂,但每个都能让你多花半天时间。提前知道,值。
踩过类似的坑?说出来大家一起长个教训。
下期预告:数据类型专场
关于作者
写了 10 年 Python,赶上了机器学习的热潮,又撞上了大模型的浪头,每次以为学明白了,行业又变了一次。不是什么大神,就是一个在互联网里摸爬滚打、把坑踩了个遍的老工人。还在写代码,还在折腾,还在想明白这个时代到底发生了什么。把自己踩过的坑、走过的弯路、看过的热闹,说给还在路上的人听。
关注「鲁叶的Python」,一起穿越迷茫期。