跳到主要内容

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 的"贴标签":传参的本质是把标签复制一份贴给形参——scoresmy_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_resetnumsdata 是同一列表的两张标签,append 直接改盒子里的内容,外面看得见
  • try_replacenums = [7, 7, 7]赋值——给局部标签 nums 换了个新盒子贴,data 还贴在原盒子上,毫发无损

一句话分辨:在函数里"动盒子里的东西"(append/remove/排序/改元素)影响外面;"给标签换盒子"(整个重新赋值)不影响外面。

本章小结

  • 局部变量:函数内创建,函数内有效,函数结束即销毁;不同函数互不干扰
  • 全局变量:函数内可以读;但函数内赋值一律新建局部变量(这就是 UnboundLocalError 的来源)
  • global 可以强行改全局变量,但首选"参数进、返回值出",让数据流动透明
  • 传列表/字典进函数,函数内的修改会影响外面(同一盒子两张标签);不想被改传 .copy()

函数三节全部学完!你已经掌握了组织代码的核心武器。下一章学习如何使用别人写好的代码:6.1 模块