1
2
3
4
5
6
7
作者:李晓辉

联系方式:

1. 微信:Lxh_Chat

2. 邮箱:939958092@qq.com
  1. Python 系列(一) 从零开始认识 Python,入门其实没那么难
  2. Python 系列(二) 学 Python 的第一步,把开发环境搭建好
  3. Python 系列(三) 基础语法避坑指南,缩进、大小写、续行与注释
  4. Python 系列(四) Python 输入输出详解,print()、input() 与常见避坑指南
  5. Python 系列(五) 数值类型与运算符详解,整除、浮点精度与进制转换实战
  6. Python 系列(六) 字符串处理实战,查找、替换、切片与日志分析
  7. Python 系列(七) 类型转换详解,int()、float()、str() 与金额计算避坑
  8. Python 系列(八) Python 流程控制入门,掌握 if、elif 和 else
  9. Python 系列(九) Python 逻辑运算符详解,and、or、not 一篇搞懂
  10. Python 系列(十) while、for 循环实战,彻底搞懂 range、break 与 continue
  11. Python 系列(十一) list、tuple、set、dict,到底该怎么选?
  12. Python 系列(十二) List 列表实战,搞定数据增删改查与批量处理
  13. Python 系列(十三) Tuple 元组实战,搞懂不可变特性与 enumerate() 遍历
  14. Python 系列(十四) Set 集合实战,搞定数据去重与集合运算
  15. Python 系列(十五) Dict 字典详解,掌握快速查找与键值操作
  16. Python 系列(十六) sort() 和 sorted() 详解,掌握数据排序技巧
  17. Python 系列(十七) 自定义函数入门,让重复代码一次编写多次使用
  18. Python 系列(十八) 函数参数详解,搞懂位置参数、关键字参数和默认参数
  19. Python 系列(十九) 从 *args 到 **kwargs,让函数调用更灵活
  20. Python 系列(二十) 变量作用域是什么?一文搞懂 global 和 nonlocal
  21. Python 系列(二十一) 搞懂变量、对象和引用,理解函数参数传递机制
  22. Python 系列(二十二) map() 和 filter() 详解,轻松实现批量数据处理与筛选
  23. Python 系列(二十三) 嵌套函数与 lambda 表达式详解,搞懂作用域与闭包

前面我们学习了 Python 函数的位置参数、关键字参数、默认参数,以及 *args 和 **kwargs 等可变参数。

现在,我们已经能够编写函数,也知道如何灵活地向函数传递数据了。

但接下来还有一个问题:函数中定义的变量,为什么有时候在函数外面无法访问?为什么修改一个变量,还需要使用 global 或 nonlocal?

比如,我们想统计脚本检查了多少台服务器,能不能直接在函数中修改外面的计数器?如果一个函数里面还有另一个函数,内部函数又该如何修改外部函数中的变量?

这些问题都与 Python 的变量作用域有关。

什么是变量作用域?

先看一个简单的例子:

1
2
3
4
5
def check_server():
server_name = "web-01"
print(server_name)

check_server()

运行结果:

1
web-01

这里定义了一个变量 server_name,它位于 check_server() 函数内部。

如果我们尝试在函数外部访问它:

1
2
3
4
5
6
7
def check_server():
server_name = "web-01"
print(server_name)

check_server()

print(server_name)

运行后,程序会抛出 NameError,提示 server_name 没有定义。

1
2
3
4
5
Traceback (most recent call last):
File "<python-input-1>", line 7, in <module>
print(server_name)
^^^^^^^^^^^
NameError: name 'server_name' is not defined

为什么会这样?

因为 server_name 是函数内部定义的局部变量。函数外部无法直接通过这个名称访问函数内部的局部绑定。

我们可以把变量作用域理解为:一个名称在哪些代码区域内可以被访问,以及它的绑定属于哪个作用域。

在 Python 中,不同位置定义的名称,其作用域可能不同。

  • 函数内部定义的名称,通常属于局部作用域。
  • 模块顶层定义的名称,通常属于全局作用域。
  • 嵌套函数可以访问符合规则的外层函数作用域中的名称。
  • Python 还提供了内置作用域,其中包含 print()、len() 等内置名称。

理解这些规则,就能解释为什么有些名称可以直接使用,有些名称却需要借助 global 或 nonlocal。

Python 如何查找变量?LEGB 规则

Python 中常用 LEGB 来描述函数中普通名称的查找顺序。

LEGB 分别代表:

名称英文含义
LLocal当前函数的局部作用域
EEnclosing外层嵌套函数的作用域
GGlobal当前模块的全局作用域
BBuilt-inPython 内置作用域

当函数需要读取一个普通名称时,可以先从当前局部作用域查找;如果没有找到,再依次检查外层函数、全局作用域和内置作用域。

flowchart TD
    A["读取一个名称"] --> B{"局部作用域 L"}
    B -->|找到| R["使用该名称绑定"]
    B -->|未找到| C{"外层函数作用域 E"}
    C -->|找到| R
    C -->|未找到| D{"全局作用域 G"}
    D -->|找到| R
    D -->|未找到| E{"内置作用域 B"}
    E -->|找到| R
    E -->|未找到| F["抛出 NameError"]

来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
name = "Global"

def outer():
name = "Enclosing"

def inner():
name = "Local"
print(name)

inner()

outer()

运行结果:

1
Local

inner() 函数中存在局部变量 name,因此读取 name 时直接使用局部变量,不会继续查找外层作用域。

如果删除 inner() 中的 name = "Local",它就会使用 outer() 中的 name,输出 Enclosing。

如果外层函数也没有定义 name,Python 才会继续查找模块全局作用域。

需要注意:LEGB 描述的是普通名称的查找顺序,不是赋值时寻找变量的顺序。 Python 会根据函数中的名称绑定规则确定赋值目标,不能简单理解为赋值时也按照 LEGB 从内到外查找。

局部变量:函数内部的变量

先看最常见的局部变量:

1
2
3
4
5
def check_server():
status = "running"
print(status)

check_server()

运行结果:

1
running

变量 status 在函数内部定义,因此它属于当前函数的局部作用域。

在这个例子中,status 只供函数内部使用。如果在函数外部直接访问 status,就会因为找不到这个名称而报错。

在实际运维脚本中,下面这种写法也很常见:

1
2
3
4
5
def check_disk():
disk_usage = 85
print(f"磁盘使用率:{disk_usage}%")

check_disk()

运行结果:

1
磁盘使用率:85%

disk_usage 只是当前检查函数使用的数据,不需要让其他函数直接访问它。

把这类变量留在函数内部,可以减少不同函数之间的意外干扰,让代码更容易维护。

如果其他函数也需要使用磁盘使用率,可以通过函数返回值将结果传递出去,而不是直接访问局部变量。

全局变量:模块级别的变量

如果变量定义在函数外部、模块顶层,它通常属于全局作用域。

例如:

1
2
3
4
5
6
7
server_name = "web-01"

def check_server():
print(server_name)

check_server()
print(server_name)

运行结果:

1
2
web-01
web-01

这里的 server_name 定义在模块顶层。

函数内部没有定义同名的局部变量,因此 Python 可以在全局作用域中找到它。

这说明:函数可以读取全局变量,但读取全局变量和重新绑定全局变量是两回事。

这个区别非常重要。

为什么修改全局变量需要 global?

假设我们想统计服务器检查次数:

1
2
3
4
5
6
count = 0

def check_server():
count += 1

check_server()

你可能觉得,count 已经在函数外面定义了,函数中直接加一不就可以了吗?

实际上,这段代码会抛出 UnboundLocalError。

1
2
3
4
5
6
Traceback (most recent call last):
File "<python-input-8>", line 6, in <module>
check_server()
File "<python-input-8>", line 4, in check_server
count += 1
UnboundLocalError: cannot access local variable 'count' where it is not associated with a value

原因在于:Python 会根据函数中的名称绑定情况确定名称的作用域。由于函数中存在 count += 1 这样的赋值操作,count 会被视为当前函数中的局部名称,除非通过 global 或 nonlocal 等规则明确指定其他绑定作用域。

而 count += 1 不只是修改操作,它还需要先读取 count 原来的值。此时,函数内部的局部名称还没有绑定到一个值,因此发生错误。

这时候就需要使用 global。

使用 global 修改全局变量

修改后的代码如下:

1
2
3
4
5
6
7
8
9
10
count = 0

def check_server():
global count
count += 1

check_server()
check_server()

print(count)

运行结果:

1
2

关键就在这一行:

1
global count

它告诉 Python:当前函数中的 count 指向模块级别的全局名称绑定,而不是函数自己的局部名称。

因此,每调用一次 check_server(),全局变量 count 就会增加一次。

整个过程可以这样理解:

flowchart LR
    A["全局变量 count = 0"] --> B["调用 check_server()"]
    B --> C["声明 global count"]
    C --> D["执行 count += 1"]
    D --> E["重新绑定全局 count"]
    E --> F["count = 1"]

如果函数调用两次,最终结果就是 2。

需要注意,global 的作用是指定名称绑定到模块全局作用域。它不会把所有变量都变成全局变量,也不会改变列表、字典等对象本身的修改规则。

修改对象与重新绑定名称,有什么区别?

这里还有一个很容易混淆的地方:修改对象内部的内容,不等于重新绑定变量名称。

先看一个例子:

1
2
3
4
5
6
7
servers = ["web-01", "web-02"]

def add_server():
servers.append("web-03")

add_server()
print(servers)

运行结果:

1
['web-01', 'web-02', 'web-03']

这里没有使用 global,但列表内容确实发生了变化。

这是因为 servers.append() 修改的是列表对象内部的内容。函数读取了全局名称 servers,通过它访问列表对象,再调用 append() 修改列表,并没有将名称 servers 重新绑定到另一个对象。

再来看另一种情况:

1
2
3
4
5
6
7
8
servers = ["web-01", "web-02"]

def reset_servers():
servers = ["web-03"]
print(servers)

reset_servers()
print(servers)

运行结果:

1
2
['web-03']
['web-01', 'web-02']

虽然函数内部也使用了 servers 这个名字,但由于函数中存在赋值操作,servers 被视为局部名称。函数里的赋值创建了一个新的局部绑定,并没有改变全局名称 servers 原来的绑定。

如果希望重新绑定全局名称,可以这样写:

1
2
3
4
5
6
7
8
servers = ["web-01", "web-02"]

def reset_servers():
global servers
servers = ["web-03"]

reset_servers()
print(servers)

运行结果:

1
['web-03']

所以,判断是否需要 global,不能只看函数有没有修改函数外部的数据,而要看函数是否需要重新绑定全局名称。

在自动化运维脚本中,如果只是向已有列表追加服务器,或者修改字典中的检查结果,通常不需要 global。如果要让某个名称重新指向另一个对象,就需要考虑相应的作用域规则。

global 并不是读取变量的必需条件

如果只是读取全局变量,并不需要使用 global:

1
2
3
4
5
6
count = 10

def show_count():
print(count)

show_count()

运行结果:

1
10

这里没有在函数中给 count 赋值,因此 Python 可以从全局作用域读取它。

只有当你需要在函数内部重新绑定某个全局名称时,才需要声明 global。

例如:

1
2
3
4
5
6
7
8
status = "running"

def stop_service():
global status
status = "stopped"

stop_service()
print(status)

运行结果:

1
stopped

不过,在实际项目中,不建议为了方便就大量使用全局变量。多个函数共同修改同一个全局状态,容易让程序的执行结果变得难以追踪。

对于服务器检查次数这样的数据,也可以通过函数参数和返回值来传递,或者将状态封装到对象中。这样通常更容易测试和维护。

嵌套函数中的变量怎么办?

前面我们讨论的是函数和模块之间的变量关系。

但 Python 还支持嵌套函数,也就是在一个函数内部再定义另一个函数。

例如:

1
2
3
4
5
6
7
8
9
def outer():
message = "外层函数"

def inner():
print(message)

inner()

outer()

运行结果:

1
外层函数

虽然 message 不是 inner() 自己定义的局部变量,但 inner() 仍然可以访问它。

这是因为 LEGB 中的 E,也就是 Enclosing,表示外层嵌套函数的作用域。

当 inner() 在自己的局部作用域中找不到 message 时,就会继续向外层函数查找。

不过,能够读取外层函数中的变量,不代表可以直接给它重新赋值。

看下面的例子:

1
2
3
4
5
6
7
8
9
10
def outer():
count = 0

def inner():
count += 1

inner()
print(count)

outer()

这段代码同样会抛出 UnboundLocalError。

1
2
3
4
5
6
7
8
9
10
11
Traceback (most recent call last):
File "<python-input-16>", line 10, in <module>
outer()
~~~~~^^
File "<python-input-16>", line 7, in outer
inner()
~~~~~^^
File "<python-input-16>", line 5, in inner
count += 1
^^^^^
UnboundLocalError: cannot access local variable 'count' where it is not associated with a value

原因是 inner() 中的 count += 1 会让 Python 把 count 视为 inner() 的局部名称,而不是外层 outer() 中的名称。

这时候,我们需要另一个关键字:nonlocal。

使用 nonlocal 修改外层函数的变量

nonlocal 用于嵌套函数中,声明某个名称绑定到最近的、包含该名称的外层函数作用域,而不是当前函数的局部作用域或模块全局作用域。

修改后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
def outer():
count = 0

def inner():
nonlocal count
count += 1

inner()
inner()
return count

print(outer())

运行结果:

1
2

这里的关键是:

1
nonlocal count

它告诉 Python:当前函数中的 count 不是 inner() 自己的局部名称,而是使用外层函数中的 count。

每次调用 inner(),都会将外层的 count 增加一次。因此调用两次后,outer() 返回 2。

执行过程可以表示为:

flowchart LR
    A["调用 outer()"] --> B["定义外层 count = 0"]
    B --> C["调用 inner()"]
    C --> D["声明 nonlocal count"]
    D --> E["执行 count += 1"]
    E --> F["外层 count = 1"]
    F --> G["再次调用 inner()"]
    G --> H["外层 count = 2"]
    H --> I["return count"]

需要注意,nonlocal 只能用于存在相应外层函数绑定的嵌套作用域。如果外层函数中不存在这个名称,Python 就会报错;它不能用来声明模块级别的全局变量。

global 和 nonlocal 有什么区别?

两者都能影响函数内部名称的绑定位置,但目标作用域不同。

对比项globalnonlocal
目标作用域当前模块的全局作用域最近的、包含该名称的外层函数作用域
常见使用场景函数重新绑定模块级变量内部函数重新绑定外层函数中的变量
是否需要嵌套函数不需要需要存在合适的外层函数作用域
目标名称不存在时赋值可以创建模块级绑定必须存在符合条件的外层绑定

可以简单记住:

  • global:我要操作的是模块级别的名称绑定。
  • nonlocal:我要操作的是外层函数中的名称绑定。

再看一个对比:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
count = 0

def outer():
total = 0

def inner():
global count
nonlocal total

count += 1
total += 1

inner()
return total

print(outer())
print(count)

运行结果:

1
2
1
1

在这个例子中:

  • count 属于模块级别,因此使用 global。
  • total 属于外层函数 outer(),因此使用 nonlocal。
  • inner() 调用一次,两者各增加一次。

虽然这个例子同时使用了两个关键字,但在日常开发中,最好只在确实需要修改相应作用域中的名称绑定时才使用它们。

实际开发中应该如何选择?

在编写 Python 运维脚本时,我们经常会处理服务器列表、检查结果、执行状态等数据。

遇到变量共享问题时,可以优先考虑以下方式:

  • 只在函数内部使用的数据:定义为局部变量。
  • 需要向函数提供的数据:优先通过参数传入。
  • 需要将处理结果交给调用者的数据:优先通过 return 返回。
  • 确实需要重新绑定模块级名称时:再考虑全局变量和 global。
  • 需要内部函数维护外层函数状态时:可以考虑 nonlocal,也可以根据复杂程度使用类或其他结构封装状态。

例如,统计服务器检查次数并不一定要使用全局变量:

1
2
3
4
5
6
7
8
9
def check_server(count):
count += 1
return count

count = 0
count = check_server(count)
count = check_server(count)

print(count)

运行结果:

1
2

这里通过参数传入计数值,再通过返回值把更新后的结果交还给调用者。

这种方式的好处是,函数依赖什么数据、产生什么结果,都更加明确。

当然,global 和 nonlocal 并不是不能使用。理解它们的工作原理,才能在阅读现有代码、分析闭包或设计状态管理逻辑时做出正确选择。

写在最后

这篇文章我们学习了 Python 变量作用域,重点理解了几个问题:

  • 为什么函数内部定义的局部变量,不能直接在函数外部访问?
  • 为什么函数可以读取全局变量,却需要使用 global 才能在函数内部重新绑定它?
  • 为什么修改列表内容通常不需要 global,但重新绑定全局名称时需要考虑作用域规则?
  • 为什么嵌套函数可以读取外层函数的变量,却需要使用 nonlocal 才能重新绑定外层变量?

掌握 LEGB 查找规则后,这些现象就不再神秘了。

下一篇,我们将继续学习 Python 中一个非常重要的概念:变量、对象与引用之间的关系。

届时我们会进一步理解,为什么把一个列表传入函数后,函数内部修改列表的内容,外部也可能观察到变化;以及为什么这种现象不能简单地理解为传统意义上的“引用传递”。