Python3 命名空间和作用域:变量住在哪里,谁能找到它
我是陈默,一个正拼命上岸的码农。
你有没有遇到过这种事?
明明定义了一个变量,换个地方就找不到了。或者两个地方都叫 x,但值不一样。
这不是 bug,这是 Python 的命名空间和作用域在"管事"。
搞懂它,你就能彻底理解变量的"可见性"——谁看得见谁,谁看不见谁。
1. 命名空间:变量住在哪个房间
命名空间就是一个"名字到对象"的映射。说白了,就是一个字典,记录着每个名字对应什么值。
三种命名空间
# 1. 内置命名空间:Python 自带的print(len([1, 2, 3])) # len 是内置的print(max(1, 2, 3)) # max 也是# 2. 全局命名空间:模块级别的name = "陈默"# 全局变量score = 85# 全局变量# 3. 局部命名空间:函数内部的defgreet(): message = "你好"# 局部变量 print(message)
三种命名空间就像三层楼。
- 一楼:内置的,谁都能用(
len、print、max) - 二楼:全局的,这个文件里都能用(
name、score)
查看命名空间
# 查看全局命名空间x = 10y = 20print(globals()["x"]) # 输出: 10# 查看局部命名空间(在函数里)defshow(): a = 1 b = 2 print(locals())show() # 输出: {'a': 1, 'b': 2}
2. 作用域:变量的"有效范围"
作用域就是——一个变量在哪些地方能被访问到。
LEGB 规则
Python 查找变量时,按这个顺序找:
L → E → G → B
# B:内置# len、print、max 这些# G:全局message = "全局消息"defouter():# E:外层函数 message = "外层消息"definner():# L:局部 message = "局部消息" print(message) inner()outer() # 输出: 局部消息
Python 从内到外找。找到了就用,不再往外找了。
验证 LEGB
x = "全局的 x"defouter(): x = "外层的 x"definner(): x = "局部的 x" print(x) # 找到局部的,不再往外找 inner() print(x) # 找到外层的outer()print(x) # 找到全局的# 输出:# 局部的 x# 外层的 x# 全局的 x
把 inner() 里的 x = "局部的 x" 删掉试试:
x = "全局的 x"defouter(): x = "外层的 x"definner(): print(x) # 没有局部的 x,找外层的 inner()outer()# 输出: 外层的 x
再把 outer() 里的也删掉:
x = "全局的 x"defouter():definner(): print(x) # 外层也没有,找全局的 inner()outer()# 输出: 全局的 x
一层一层往外找。这就是 LEGB。
3. 局部变量 vs 全局变量
基本规则
count = 0# 全局变量defincrement(): count = 1# 这是局部变量!不是全局的那个 print(f"函数内:{count}")increment() # 输出: 函数内:1print(f"函数外:{count}") # 输出: 函数外:0
函数内部赋值,创建的是局部变量。不会动全局变量。
函数内可以读取全局变量
name = "陈默"defgreet(): print(f"你好,{name}") # 读取全局变量,没问题greet() # 输出: 你好,陈默
但不能直接修改全局变量
count = 0defincrement(): count += 1# 报错!UnboundLocalErrorincrement()
Python 看到函数里有 count =,就认为 count 是局部变量。但还没赋值就读了,所以报错。
4. global:告诉 Python 我要用全局变量
count = 0defincrement():global count # 声明:我要用全局的 count count += 1increment()increment()increment()print(count) # 输出: 3
global 告诉 Python:"别创建局部变量了,用外面的那个。"
什么时候用 global?
老实说,尽量少用。
# ❌ 滥用 globaltotal = 0defadd(x):global total total += x# ✅ 用返回值代替defadd(x, total):return total + xtotal = 0total = add(5, total)total = add(3, total)print(total) # 输出: 8
好的函数应该通过参数接收数据、通过返回值输出结果。不偷着改外面的变量。
5. nonlocal:修改外层函数的变量
global 改全局的,nonlocal 改外层函数的。
defcounter(): count = 0defincrement():nonlocal count # 声明:我要用外层的 count count += 1return countreturn incrementc = counter()print(c()) # 输出: 1print(c()) # 输出: 2print(c()) # 输出: 3# 创建新的计数器,互不影响c2 = counter()print(c2()) # 输出: 1
这就是闭包。nonlocal 让内部函数能修改外层函数的变量。
6. 常见陷阱
陷阱一:循环变量泄漏
x = 10for x in range(5):passprint(x) # 输出: 4(全局的 x 被循环改了!)
Python 的 for 循环不会创建新的作用域。循环变量会修改外层的同名变量。
陷阱二:列表推导式的作用域
x = "全局"# 列表推导式有自己的作用域result = [x for x in range(3)]print(x) # 输出: 全局(没被改,推导式有自己的作用域)
列表推导式、生成器表达式、字典/集合推导式,都有自己的作用域。
陷阱三:默认参数的可变对象
# ❌ 默认参数是可变对象defadd_item(item, items=[]): items.append(item)return itemsprint(add_item("苹果")) # 输出: ['苹果']print(add_item("香蕉")) # 输出: ['苹果', '香蕉'](不是空的!)# ✅ 用 None 代替defadd_item(item, items=None):if items isNone: items = [] items.append(item)return itemsprint(add_item("苹果")) # 输出: ['苹果']print(add_item("香蕉")) # 输出: ['香蕉'](正确)
默认参数在函数定义时只创建一次。可变对象(列表、字典)会被所有调用共享。
7. globals() 和 locals()
globals():查看和修改全局命名空间
x = 10y = 20# 查看所有全局变量g = globals()print(g["x"]) # 输出: 10# 甚至可以动态创建变量(不推荐,但了解一下)g["z"] = 30print(z) # 输出: 30
locals():查看局部命名空间
defshow_locals(a, b): c = a + b print(locals())show_locals(1, 2)# 输出: {'a': 1, 'b': 2, 'c': 3}
注意:在模块级别,locals() 和 globals() 是一样的。
8. 实战:理解作用域链
# 一个综合例子total = 0# 全局defprocess(data):"""处理数据,累加到 total"""global total count = 0# process 的局部defhandle(item):nonlocal countif item > 0: count += 1return item * 2return0 results = []for item in data: result = handle(item) results.append(result) total += result print(f"处理了{count}个有效数据")return resultsdata = [5, -1, 3, 0, 8, -2, 7]print(f"处理结果:{process(data)}")print(f"总计:{total}")# 输出:# 处理了4个有效数据# 处理结果:[10, 0, 6, 0, 16, 0, 14]# 总计:46
这个例子里有:
count:process 的局部变量,用 nonlocal 在 handle 中修改data、results:process 的局部变量
最后
命名空间和作用域,说白了就是回答一个问题:这个变量,在这个位置,能不能被找到?
记住三件事:
- LEGB 规则:从内到外找变量——局部 → 外层函数 → 全局 → 内置
- 函数内部赋值创建局部变量。想改全局变量,用
global;想改外层函数的变量,用 nonlocal - 尽量少用
global。用参数和返回值传递数据,代码更安全
我的建议:
打开你的编辑器,写几个嵌套函数,每个层级都定义同名变量。然后一层一层删掉,观察打印结果的变化。亲眼看到 LEGB 的查找过程,比看十遍文章都管用。
理解作用域,就是理解变量的"边界"。边界清楚了,bug 就少了。
今天就到这里。
我是陈默,我们下期再见。
如果你觉得这篇文章有帮助,欢迎关注我。我会持续分享 Python 学习的干货。