目 录CONTENT

文章目录

Python(十七) 中的作用域与 LEGB 规则

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

这里的 printlenmaxsum 都是 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 有很多内置名字,比如 liststrsummax

如果我们把变量名写成这些名字,就可能覆盖内置名字,导致后面代码出错。

不推荐:

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 使用变量时,会按照“局部、外层、全局、内置”的顺序去查找名字。
0
博主关闭了当前页面的评论