5.3 变量作用域
函数最后一节,回答一个之前被悄悄绕过的问题:函数里的变量和函数外的变量,是什么关系?
先抛一个直觉测试。你觉得这段代码输出什么?
x = 10
def change():
x = 20
change()
print(x) # 猜猜是 10 还是 20?
答案是 10。函数里明明写了 x = 20,外面的 x 却纹丝不动——理解这件事,就是这一节的全部任务。**作用域(scope)**这个词回答的就是:一个变量,在哪些地方"看得见、用得了"。
局部变量:函数内部的私有空间
在函数里面创建的变量是局部变量,只在函数内部有效。函数一执行完,它就被销毁:
def calc():
result = 100 # 局部变量:诞生在函数里
print(result) # ✅ 函数内随便用
calc()
print(result) # ❌ NameError: name 'result' is not defined
从外面看,result 就像从没存在过。可以把每次函数调用想象成支起一间临时帐篷:帐篷里造的东西归帐篷,函数结束帐篷拆掉,里面的东西全部清空。
由此推论:不同函数的局部变量互不相干,重名也没事:
def func_a():
x = 1 # func_a 帐篷里的 x
def func_b():
x = 999 # func_b 帐篷里的 x,和上面那个毫无关系
别嫌这是限制——它是天大的好事。正因为有隔离,你写函数时才不用担心"我的变量名会不会跟别人 500 行外的代码撞车"。函数参数也是局部变量:def f(n): 里的 n 同样只活在函数里。
全局变量:写在最外层的变量
不在任何函数里、顶格写的变量是全局变量,整个文件里都看得见,函数内部也能读:
site_name = "Python学习网" # 全局变量
def show_title():
print(f"欢迎来到{site_name}") # ✅ 函数内读全局变量,没问题
show_title() # 欢迎来到Python学习网
查找规则:函数里用到一个变量时,Python 先找自己帐篷里有没有(局部),没有再去外面找(全局),都没有才报 NameError。
关键规则:函数内赋值 = 创建局部变量
现在可以解释开头的谜题了。规则只有一句:
在函数内给变量赋值,Python 一律视为"创建一个新的局部变量"——哪怕外面有同名的全局变量。
x = 10
def change():
x = 20 # 这不是修改全局 x!是在帐篷里新建了一个局部 x
print(x) # 20 ← 帐篷里当然用帐篷里的
change()
print(x) # 10 ← 全局 x 从头到尾没被碰过
函数里的 x = 20 和全局的 x = 10 是两个不同的变量,只是恰好重名,就像两个班都有人叫"小明"。
这条规则还有个著名的衍生报错。想在函数里给全局计数器加一:
count = 0
def add_count():
count = count + 1 # ❌ UnboundLocalError: local variable 'count' referenced before assignment
add_count()
为什么连读都不让读了?因为 Python 看到函数里有对 count 的赋值,就认定 count 是局部变量;而执行 count + 1 时这个局部 count 还没有值——"局部变量在赋值前就被使用",正是 UnboundLocalError 的意思。看到这个报错,几乎可以断定:你想改全局变量,但没告诉 Python。
global:声明"我要改的是全局那个"
真要在函数里修改全局变量,用 global 提前声明:
count = 0
def add_count():
global count # 声明:下面的 count 就是全局那个,别新建局部的
count = count + 1
add_count()
add_count()
print(count) # 2
但是:能不用 global 就不用
global 能解决问题,却是公认的坏味道。想象程序里有 20 个函数都 global count 改来改去——某天 count 的值不对了,你得把 20 个函数全查一遍才能知道是谁改的。数据的流动路径越隐蔽,bug 越难查。
推荐的风格是 5.1 就铺垫过的:参数进、返回值出,让数据的流动明明白白:
# 不推荐:靠 global 暗中沟通
count = 0
def add_count():
global count
count += 1
# 推荐:靠参数和返回值明面沟通
def add_count(count):
return count + 1
count = 0
count = add_count(count) # 数据进去、结果出来,一目了然
count = add_count(count)
print(count) # 2
原则:函数应该像一台机器——原料从参数进,成品从 return 出,不偷偷动车间外的东西。 读全局配置(如 site_name)无妨,改就要三思。
隐藏关卡:传列表进函数,函数能改动它!
上面说"函数不影响外面",但有一个重要例外,直接看实验:
def add_score(scores):
scores.append(100) # 在函数里往列表塞了个 100
my_scores = [90, 85]
add_score(my_scores)
print(my_scores) # [90, 85, 100] ——外面的列表真的变了!
数字传进函数改不动外面,列表怎么就改得动?还是 4.1 的"贴标签":传参的本质是把标签复制一份贴给形参——scores 和 my_scores 是同一个列表的两张标签。函数里通过 scores.append() 往盒子里塞东西,外面透过 my_scores 当然看得见。
而数字、字符串是不可变的,函数里 n = n + 1 只会让局部标签换个新盒子贴,外面的原盒子安然无恙。
这个特性是双刃剑:
- 用好了是功能:"请函数帮我整理这个列表"(比如
lst.sort()就是这么工作的) - 没意识到就是 bug:函数悄悄改了你的数据。不想被改就传副本:
add_score(my_scores.copy())
本节报错速查
| 报错 | 人话翻译 | 本节的触发场景 |
|---|---|---|
NameError | 名字不存在 | 在函数外面用局部变量 |
UnboundLocalError | 局部变量赋值前就被使用 | 函数内想"读改"全局变量却没声明 global |
练习
新建 practice53.py 完成:
1. 不运行,先猜输出,再验证:
x = 10
def change():
x = 20
print("函数内:", x)
change()
print("函数外:", x)
2. 把第 1 题改成用 global,让函数外打印出 20。再想想:如果不许用 global,怎么用"参数进、返回值出"达到同样效果?
3. 不运行,先猜输出,再验证(小心陷阱):
def try_reset(nums):
nums.append(0)
def try_replace(nums):
nums = [7, 7, 7]
data = [1, 2]
try_reset(data)
print(data)
try_replace(data)
print(data)
点击查看答案
第 1 题: 输出 函数内: 20 和 函数外: 10。函数内的赋值创建了局部 x,不影响全局 x。
# 2 —— global 版
x = 10
def change():
global x
x = 20
change()
print(x) # 20
# 2 —— 推荐版:参数进、返回值出
def change(val):
return 20
x = 10
x = change(x)
print(x) # 20
第 3 题: 输出 [1, 2, 0] 和 [1, 2, 0]。
try_reset:nums和data是同一列表的两张标签,append直接改盒子里的内容,外面看得见try_replace:nums = [7, 7, 7]是赋值——给局部标签nums换了个新盒子贴,data还贴在原盒子上,毫发无损
一句话分辨:在函数里"动盒子里的东西"(append/remove/排序/改元素)影响外面;"给标签换盒子"(整个重新赋值)不影响外面。
本章小结
- 局部变量:函数内创建,函数内有效,函数结束即销毁;不同函数互不干扰
- 全局变量:函数内可以读;但函数内赋值一律新建局部变量(这就是 UnboundLocalError 的来源)
global可以强行改全局变量,但首选"参数进、返回值出",让数据流动透明- 传列表/字典进函数,函数内的修改会影响外面(同一盒子两张标签);不想被改传
.copy()
函数三节全部学完!你已经掌握了组织代码的核心武器。下一章学习如何使用别人写好的代码:6.1 模块