Python(十七) 中的作用域与 LEGB 规则
一、什么是作用域
作用域,英文叫 scope。
在 Python 中,作用域指的是一个变量可以被访问和使用的范围。
通俗地说:
作用域就是变量的“有效范围”。
一个变量不是在任何地方都能用。
它在哪里定义,通常就决定了它在哪里可以使用。
生活中的例子:
班级里的通知,只对这个班的学生有效。
学校里的通知,对整个学校有效。
城市里的通知,对整个城市有效。
变量也是类似的:
有些变量只在函数内部有效。
有些变量在整个文件中都能使用。
有些名字是 Python 自带的,到处都可以使用。
示例:
def say_hello():
name = "小明"
print(name)
say_hello()
这里的 name 是在函数内部定义的变量。
它只能在 say_hello() 函数内部使用。
如果在函数外面使用它:
def say_hello():
name = "小明"
say_hello()
print(name)
程序会报错,因为函数外面找不到 name。
二、为什么要学习作用域
学习作用域很重要,因为它能帮助我们理解:
1. 为什么有些变量在某些地方能用,在某些地方不能用
2. 为什么函数内部修改变量时,有时会报错
3. global 和 nonlocal 到底什么时候用
4. 为什么不建议随便使用全局变量
5. Python 查找变量时到底按什么顺序查找
如果不理解作用域,写代码时很容易遇到下面这些问题:
NameError:变量没有定义
UnboundLocalError:局部变量还没赋值就被使用
变量名冲突
函数内部和外部变量互相影响
三、什么是 LEGB 规则
LEGB 是 Python 查找变量时遵循的一套顺序。
当程序使用一个变量名时,Python 会按照下面顺序去找:
L:Local,本地作用域,也叫局部作用域
E:Enclosing,嵌套函数外层作用域
G:Global,全局作用域
B:Built-in,内置作用域
合起来就是:
Local -> Enclosing -> Global -> Built-in
可以这样记:
先找自己家。
再找外层家。
再找整个文件。
最后找 Python 自带的名字。
如果这四个地方都找不到变量,程序就会报错:
NameError
四、Local 本地作用域
Local 作用域指函数内部的作用域。
在函数内部定义的变量,通常就是局部变量。
示例:
def test():
x = 10
print(x)
test()
输出结果:
10
这里的 x 是局部变量。
它只在 test() 函数内部有效。
如果在函数外面访问:
def test():
x = 10
test()
print(x)
会报错:
NameError: name 'x' is not defined
原因:
x 定义在函数内部。
函数外部看不到这个局部变量。
教学时可以这样讲:
局部变量就像放在房间里的东西。
房间里面的人可以用。
房间外面的人看不到。
五、Global 全局作用域
Global 作用域指当前 Python 文件中的作用域。
在函数外部定义的变量,通常就是全局变量。
示例:
name = "小明"
def say_name():
print(name)
say_name()
输出结果:
小明
这里的 name 是全局变量。
函数内部没有定义 name,Python 会继续到全局作用域中查找。
所以函数内部可以读取全局变量。
再看一个例子:
count = 100
def show_count():
print(count)
show_count()
print(count)
输出结果:
100
100
说明:
全局变量可以在函数外部使用。
函数内部也可以读取它。
六、Built-in 内置作用域
Built-in 作用域指 Python 内置的名字。
例如:
print
len
sum
max
min
int
str
list
dict
range
这些名字不需要我们自己定义,就可以直接使用。
示例:
numbers = [1, 2, 3]
print(len(numbers))
print(max(numbers))
print(sum(numbers))
输出结果:
3
3
6
这里的 print、len、max、sum 都是 Python 内置名字。
如果 Local、Enclosing、Global 都没有找到某个名字,Python 最后会去 Built-in 作用域中找。
七、Enclosing 嵌套函数外层作用域
Enclosing 作用域出现在函数里面再定义函数的情况。
也就是嵌套函数。
示例:
def outer():
x = "外层函数的变量"
def inner():
print(x)
inner()
outer()
输出结果:
外层函数的变量
执行过程:
inner() 内部没有定义 x。
Python 先在 inner 的 Local 作用域中找 x,没有找到。
然后去外层函数 outer 的 Enclosing 作用域中找,找到了 x。
这里的 outer() 对 inner() 来说,就是外层函数。
可以这样理解:
Local:inner 自己里面
Enclosing:outer 这个外层函数里面
Global:整个文件里面
Built-in:Python 内置名字里面
八、LEGB 查找顺序示例
下面通过一个完整例子理解 LEGB:
x = "全局变量"
def outer():
x = "外层函数变量"
def inner():
x = "内层函数变量"
print(x)
inner()
outer()
输出结果:
内层函数变量
原因:
print(x) 在 inner() 函数内部。
Python 先在 inner 的 Local 作用域中找 x。
因为 inner 里面有 x = "内层函数变量",所以直接使用它。
后面的 Enclosing 和 Global 就不会再找了。
如果去掉 inner 里面的 x:
x = "全局变量"
def outer():
x = "外层函数变量"
def inner():
print(x)
inner()
outer()
输出结果:
外层函数变量
原因:
inner 的 Local 作用域中没有 x。
Python 去 Enclosing 作用域,也就是 outer 中找。
outer 中有 x,所以使用 outer 中的 x。
如果再去掉 outer 里面的 x:
x = "全局变量"
def outer():
def inner():
print(x)
inner()
outer()
输出结果:
全局变量
原因:
Local 没有找到。
Enclosing 没有找到。
Global 中找到了 x。
如果全局作用域也没有 x:
def outer():
def inner():
print(x)
inner()
outer()
程序会报错:
NameError: name 'x' is not defined
原因:
Local、Enclosing、Global、Built-in 都没有找到 x。
九、局部变量和全局变量同名
如果局部变量和全局变量同名,函数内部会优先使用局部变量。
示例:
name = "全局的小明"
def test():
name = "局部的小红"
print(name)
test()
print(name)
输出结果:
局部的小红
全局的小明
解释:
函数内部 print(name) 先在 Local 作用域中找 name。
函数内部有局部变量 name,所以输出“局部的小红”。
函数外部 print(name) 使用的是全局变量 name。
这说明:
局部变量和全局变量可以同名。
但同名时,局部变量会遮住全局变量。
教学提醒:
为了减少混乱,不建议让局部变量和全局变量随便同名。
十、函数内部读取全局变量
函数内部可以直接读取全局变量。
示例:
count = 10
def show():
print(count)
show()
输出结果:
10
因为函数内部没有定义 count,所以 Python 会去全局作用域中找。
但是,读取全局变量和修改全局变量是不一样的。
十一、函数内部修改全局变量
如果在函数内部直接给变量赋值,Python 默认会认为这个变量是局部变量。
示例:
count = 10
def add():
count = 20
print("函数内部:", count)
add()
print("函数外部:", count)
输出结果:
函数内部: 20
函数外部: 10
解释:
函数内部的 count = 20 创建了一个新的局部变量 count。
它没有修改外面的全局变量 count。
如果确实想在函数内部修改全局变量,需要使用 global。
十二、global 关键字
global 用来声明:函数内部使用的是全局变量。
示例:
count = 10
def add():
global count
count = count + 1
add()
print(count)
输出结果:
11
解释:
global count 表示函数内部的 count 是全局变量 count。
所以 count = count + 1 会修改全局变量。
如果不写 global,下面代码会报错:
count = 10
def add():
count = count + 1
add()
会报类似错误:
UnboundLocalError: local variable 'count' referenced before assignment
原因:
函数内部有 count = count + 1。
只要函数中出现了对 count 的赋值,Python 就会把 count 当作局部变量。
但是在执行 count + 1 时,局部变量 count 还没有值。
所以报错。
教学时可以这样讲:
Python 看到函数里有赋值,就会先把这个名字当成局部变量。
如果赋值前又使用它,就会出问题。
十三、global 的注意事项
1. global 要写在使用变量之前
正确写法:
count = 0
def add():
global count
count = count + 1
不推荐或可能报错的写法:
count = 0
def add():
print(count)
global count
count = count + 1
global 应该放在函数开头比较清楚。
2. 不要滥用 global
虽然 global 可以修改全局变量,但不建议经常使用。
不推荐:
score = 0
def add_score():
global score
score = score + 10
更推荐:
def add_score(score):
return score + 10
score = 0
score = add_score(score)
原因:
函数通过参数接收数据,通过 return 返回结果,逻辑更清楚。
全局变量被很多函数修改时,程序会变得难以排查。
可以这样告诉学生:
global 能用,但不要随便用。
函数最好少依赖外面的变量。
十四、nonlocal 关键字
nonlocal 用来声明:当前变量不是局部变量,而是外层函数中的变量。
它主要用于嵌套函数。
示例:
def outer():
count = 0
def inner():
nonlocal count
count = count + 1
print(count)
inner()
inner()
outer()
输出结果:
1
2
解释:
count 定义在 outer() 中。
inner() 中想修改 outer() 的 count。
这时需要使用 nonlocal count。
如果不写 nonlocal:
def outer():
count = 0
def inner():
count = count + 1
print(count)
inner()
outer()
会报错:
UnboundLocalError
原因和 global 类似:
inner() 中出现了 count = count + 1。
Python 会把 count 当作 inner() 的局部变量。
但执行 count + 1 时,inner() 的局部变量 count 还没有值。
十五、global 和 nonlocal 的区别
可以用表格理解:
关键字 作用
global 声明变量来自全局作用域
nonlocal 声明变量来自外层函数作用域
示例对比:
count = 0
def add_global():
global count
count = count + 1
这里的 count 是全局变量。
再看 nonlocal:
def outer():
count = 0
def inner():
nonlocal count
count = count + 1
inner()
这里的 count 是外层函数 outer() 中的变量。
简单记忆:
global 找整个文件中的变量。
nonlocal 找外层函数中的变量。
注意:
nonlocal 不能直接用于全局变量。
它必须对应某个外层函数中的变量。
十六、闭包中的作用域
闭包是函数作用域中的一个常见现象。
当一个内部函数使用了外部函数的变量,并且外部函数把内部函数返回出去时,就形成了闭包。
示例:
def make_counter():
count = 0
def counter():
nonlocal count
count = count + 1
return count
return counter
c = make_counter()
print(c())
print(c())
print(c())
输出结果:
1
2
3
解释:
make_counter() 执行结束后,count 并没有马上消失。
因为内部函数 counter() 还在使用它。
每次调用 c(),都会修改同一个 count。
初学阶段不需要把闭包讲得太深。
可以简单告诉学生:
内部函数可以记住外层函数中的变量。
这和 Enclosing 作用域有关。
十七、可变对象和作用域
作用域和“可变对象”结合时,也容易让学生困惑。
先看一个例子:
items = []
def add_item():
items.append("Python")
add_item()
print(items)
输出结果:
['Python']
这里没有使用 global,为什么函数内部还能修改全局列表?
原因:
函数内部没有给 items 重新赋值。
只是通过 items.append() 修改了列表里面的内容。
Python 先找到全局变量 items,然后修改这个列表对象。
但是如果这样写:
items = []
def add_item():
items = ["Python"]
add_item()
print(items)
输出结果:
[]
原因:
items = ["Python"] 是重新赋值。
函数内部创建了一个新的局部变量 items。
外面的全局 items 没有被修改。
如果想在函数内部重新给全局变量 items 赋值,需要使用 global:
items = []
def add_item():
global items
items = ["Python"]
add_item()
print(items)
输出结果:
['Python']
教学时可以这样讲:
append 是修改列表内容。
= 是重新绑定变量名。
这两件事不一样。
十八、不要覆盖内置名字
Python 有很多内置名字,比如 list、str、sum、max。
如果我们把变量名写成这些名字,就可能覆盖内置名字,导致后面代码出错。
不推荐:
list = [1, 2, 3]
numbers = list((4, 5, 6))
这段代码会出问题,因为 list 原本是 Python 内置函数,现在被我们当作变量名使用了。
再比如:
sum = 100
numbers = [1, 2, 3]
print(sum(numbers))
这也会出问题,因为 sum 被变量覆盖了。
推荐写法:
numbers = [1, 2, 3]
total = sum(numbers)
print(total)
教学提醒:
不要用 list、str、sum、max、min、dict 这些内置名字当变量名。
十九、常见错误 1:NameError
NameError 通常表示变量名没有找到。
示例:
print(username)
如果前面没有定义过 username,就会报错:
NameError: name 'username' is not defined
原因:
Python 按 LEGB 顺序查找 username。
Local 没有。
Enclosing 没有。
Global 没有。
Built-in 也没有。
所以报错。
解决办法:
username = "小明"
print(username)
二十、常见错误 2:UnboundLocalError
UnboundLocalError 通常和函数内部变量赋值有关。
示例:
count = 10
def add():
count = count + 1
print(count)
add()
这段代码会报错。
原因:
函数内部出现了 count = count + 1。
Python 认为 count 是局部变量。
但是在计算 count + 1 时,局部变量 count 还没有值。
解决方式一:使用参数和返回值,更推荐。
def add(count):
return count + 1
count = 10
count = add(count)
print(count)
解决方式二:使用 global。
count = 10
def add():
global count
count = count + 1
add()
print(count)
教学建议优先讲第一种,因为它更清楚。
二十一、常见错误 3:以为函数内部变量会影响外部
示例:
def change_name():
name = "小红"
name = "小明"
change_name()
print(name)
输出结果:
小明
很多初学者会以为输出“小红”。
实际原因:
change_name() 中的 name 是局部变量。
外面的 name 是全局变量。
它们只是名字相同,但不是同一个变量。
如果希望函数处理后得到新值,更推荐使用 return:
def change_name():
return "小红"
name = "小明"
name = change_name()
print(name)
输出结果:
小红
二十二、常见错误 4:滥用全局变量
全局变量看起来方便,但如果到处修改,会让程序变得难以理解。
不推荐:
score = 0
def add_score():
global score
score = score + 10
def reduce_score():
global score
score = score - 5
add_score()
reduce_score()
print(score)
这段代码可以运行,但如果函数很多,就很难知道 score 在哪里被改过。
更清楚的写法:
def add_score(score):
return score + 10
def reduce_score(score):
return score - 5
score = 0
score = add_score(score)
score = reduce_score(score)
print(score)
这样数据从哪里来、到哪里去更清楚。
二十三、课堂示例
示例 1:局部变量
def test():
message = "函数内部变量"
print(message)
test()
讲解重点:
message 定义在函数内部,只能在函数内部使用。
示例 2:读取全局变量
school = "第一中学"
def show_school():
print(school)
show_school()
讲解重点:
函数内部没有 school,就会到全局作用域中查找。
示例 3:局部变量和全局变量同名
name = "全局变量"
def test():
name = "局部变量"
print(name)
test()
print(name)
输出结果:
局部变量
全局变量
讲解重点:
函数内部优先使用局部变量。
示例 4:使用 global 修改全局变量
count = 0
def add():
global count
count = count + 1
add()
add()
print(count)
输出结果:
2
讲解重点:
global 声明函数内部使用的是全局变量。
示例 5:使用 nonlocal 修改外层函数变量
def outer():
count = 0
def inner():
nonlocal count
count = count + 1
print(count)
inner()
inner()
outer()
输出结果:
1
2
讲解重点:
nonlocal 用于嵌套函数,表示使用外层函数中的变量。
示例 6:可变对象的修改
students = []
def add_student(name):
students.append(name)
add_student("小明")
add_student("小红")
print(students)
输出结果:
['小明', '小红']
讲解重点:
append 是修改列表内容,不是重新给变量赋值。
二十四、课堂练习
练习 1:判断变量作用域
请判断下面代码的输出结果:
x = 10
def test():
x = 20
print(x)
test()
print(x)
参考答案:
20
10
练习 2:补全 global
要求:让 count 每次调用函数时加 1。
参考代码:
count = 0
def add():
global count
count = count + 1
add()
add()
print(count)
练习 3:改写代码,避免使用 global
原代码:
score = 80
def add_score():
global score
score = score + 10
推荐改写:
def add_score(score):
return score + 10
score = 80
score = add_score(score)
print(score)
练习 4:理解 nonlocal
请判断下面代码的输出结果:
def outer():
x = 1
def inner():
nonlocal x
x = x + 1
print(x)
inner()
inner()
outer()
参考答案:
2
3
练习 5:不要覆盖内置函数
请找出下面代码的问题:
sum = 100
numbers = [1, 2, 3]
print(sum(numbers))
参考说明:
sum 原本是 Python 内置函数。
这里把 sum 当作变量名使用,覆盖了内置函数。
应该换成 total 等变量名。
二十五、教学建议
讲解作用域和 LEGB 规则时,可以按照下面顺序:
1. 先从“变量在哪里能用”引出作用域
2. 讲局部变量和全局变量
3. 再讲 Python 查找变量的 LEGB 顺序
4. 用多个 x 的例子演示查找顺序
5. 讲函数内部读取全局变量和修改全局变量的区别
6. 讲 global
7. 讲嵌套函数和 nonlocal
8. 最后讲常见错误和命名注意事项
可以用下面的问题引导学生:
函数内部定义的变量,函数外面能不能用?
函数里面能不能读取函数外面的变量?
读取全局变量和修改全局变量是不是一回事?
如果函数里面和函数外面都有 x,Python 会用哪个?
为什么不要把变量名写成 list、sum、str?
教学重点建议放在:
局部变量
全局变量
LEGB 查找顺序
global 的作用
nonlocal 的作用
UnboundLocalError 的原因
不要覆盖内置名字
闭包可以作为拓展内容,初学阶段不必讲得太深。
二十六、总结
作用域指变量可以被访问和使用的范围。
Python 查找变量时遵循 LEGB 规则:
L:Local,本地作用域,也就是当前函数内部
E:Enclosing,嵌套函数的外层函数作用域
G:Global,全局作用域,也就是当前文件
B:Built-in,内置作用域,也就是 Python 自带名字
查找顺序是:
Local -> Enclosing -> Global -> Built-in
可以这样记:
先找当前函数。
再找外层函数。
再找当前文件。
最后找 Python 自带的名字。
重要注意事项:
1. 函数内部定义的变量通常是局部变量
2. 函数外部定义的变量通常是全局变量
3. 函数内部可以读取全局变量
4. 函数内部修改全局变量通常需要 global
5. 嵌套函数中修改外层函数变量需要 nonlocal
6. 局部变量和全局变量同名时,局部变量优先
7. 不要随便覆盖 Python 内置名字
8. 尽量少用 global,多用参数和 return 传递数据
一句话总结:
LEGB 规则告诉我们:Python 使用变量时,会按照“局部、外层、全局、内置”的顺序去查找名字。