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 表达式详解,搞懂作用域与闭包
  24. Python 系列(二十四) 搞懂递归函数,从基线条件到实际应用

上一篇我们学习了 Python 的 map() 和 filter(),知道了如何批量转换数据、筛选符合条件的记录。

不过,函数的用法还不止这些。

假设你正在编写一个服务器巡检脚本,需要检查 CPU、内存和磁盘使用率。不同的检查任务可能有不同的阈值,但检查逻辑又有很多相似之处。有没有办法把公共逻辑封装起来,同时让不同的检查任务拥有自己的配置?

再比如,某个函数只在另一个函数内部使用,我们有必要把它定义成一个独立的全局函数吗?如果只是简单地处理两个数字,又有没有更简洁的写法?

这就涉及今天要学习的两个知识点:嵌套函数和 lambda 表达式。

学完这篇文章,你不仅要知道它们怎么写,还要理解变量是如何查找的、为什么会出现作用域陷阱,以及闭包究竟保存了什么。

嵌套函数是什么

所谓嵌套函数,就是在一个函数的内部定义另一个函数。

先看一个最简单的例子:

1
2
3
4
5
6
7
8
9
def outer(x):
def inner(y):
return x + y

return inner


f = outer(10)
print(f(5))

输出结果:

1
15

这段代码并不复杂,但有几个细节值得注意。

首先,outer() 是外部函数,inner() 是定义在它内部的函数。

1
2
3
def outer(x):
def inner(y):
return x + y

调用 outer(10) 时,参数 x 的值为 10。此时,Python 执行 outer() 函数中的代码,并创建内部函数 inner()。

接下来:

1
return inner

这里返回的是函数对象 inner,而不是调用它的结果。

注意,两种写法有明显区别:

1
return inner

返回函数对象,调用方可以稍后再执行它。

1
return inner(5)

立即调用 inner(5),返回计算结果。

回到原来的代码:

1
f = outer(10)

f 接收了 outer() 返回的函数对象,因此可以继续调用:

1
print(f(5))

这相当于调用内部函数,并传入参数 y = 5。

最终计算:

1
x + y

也就是 10 + 5,得到 15。

整个过程可以用下面的流程理解:

flowchart LR
    A["调用 outer(10)"] --> B["创建内部函数 inner"]
    B --> C["返回 inner 函数对象"]
    C --> D["变量 f 引用该函数"]
    D --> E["调用 f(5)"]
    E --> F["读取 x = 10 和 y = 5"]
    F --> G["返回 15"]

这里有一个关键问题:调用 outer() 后,outer() 已经执行完毕,为什么内部函数仍然能够使用参数 x?

这就涉及 Python 的作用域和闭包。

作用域:Python 到底在哪里查找变量

作用域可以理解为名称在程序中可以被访问的范围。

在学习嵌套函数之前,先明确一个容易混淆的地方:变量名的查找规则,与变量赋值时的名称绑定规则,不是一回事。

我们先从普通的变量查找开始。

理解 LEGB 名称查找规则

在函数中读取一个名称时,可以使用 LEGB 作为理解名称查找过程的简化模型:

  • L(Local):当前函数的局部作用域。
  • E(Enclosing):外围函数的作用域。
  • G(Global):当前模块的全局作用域。
  • B(Built-in):Python 内置名称所在的作用域。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
message = "全局变量"


def outer():
message = "外部函数变量"

def inner():
message = "内部函数变量"
print(message)

inner()


outer()

输出结果:

1
内部函数变量

为什么输出的是“内部函数变量”?

因为 inner() 中存在一个名为 message 的局部变量。读取这个名称时,Python 首先在当前函数的局部作用域中找到了它,因此不需要继续查找外围函数或全局作用域。

再看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
message = "全局变量"


def outer():
message = "外部函数变量"

def inner():
print(message)

inner()


outer()

输出结果:

1
外部函数变量

这次,inner() 内部没有为 message 创建新的局部绑定,因此在执行这条读取语句时,Python 会继续查找外围函数作用域,最终找到 outer() 中的 message。

如果外围函数也没有这个名称,查找还可以继续到全局作用域和内置作用域。

可以将这种普通名称读取的查找过程理解为:

flowchart TD
    A["读取名称"] --> B{"当前函数局部作用域 L"}
    B -->|找到| Z["使用该名称绑定"]
    B -->|未找到| C{"外围函数作用域 E"}
    C -->|找到| Z
    C -->|未找到| D{"模块全局作用域 G"}
    D -->|找到| Z
    D -->|未找到| E{"内置作用域 B"}
    E -->|找到| Z
    E -->|未找到| F["引发 NameError 等名称查找错误"]

注意,这是一张用于理解普通名称读取的简化图。Python 会根据编译阶段确定的名称绑定规则处理函数中的局部变量,不能把 LEGB 理解成每次赋值时都从 L 开始逐层寻找并修改变量。

作用域陷阱:为什么会出现 UnboundLocalError

下面这段代码看起来似乎没有问题:

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


def outer():
def inner():
count = count + 1
print(count)

inner()


outer()

但执行后会报错:

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

为什么会这样?

很多初学者会认为:既然全局作用域已经定义了 count = 10,那么 Python 应该先读取这个值,再加上 1。

但 Python 并不是这样处理的。

在普通函数中,如果函数体内对某个名称进行了赋值,且没有通过 global 或 nonlocal 等声明改变其绑定规则,Python 通常会在编译阶段将这个名称判定为当前函数的局部变量。

例如:

1
2
def inner():
count = count + 1

Python 会将这里的 count 判定为 inner() 的局部变量。执行 count = count + 1 时,需要先读取右侧的 count,但这个局部变量此时还没有绑定值,因此会触发 UnboundLocalError。

注意:这不是 LEGB 名称查找逐层搜索后失败的结果,而是局部名称已经确定、但读取时尚未获得值所导致的错误。 Python 不会因为这个局部变量尚未赋值,就自动退回全局作用域读取同名变量。

如果确实要修改模块级的 count,可以使用 global:

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


def inner():
global count
count = count + 1
print(count)


inner()
print(count)

输出结果:

1
2
11
11

global count 声明 count 使用模块全局作用域中的绑定,因此赋值会更新全局变量。

如果要修改的是外围函数中的变量,而不是全局变量,则应该使用 nonlocal。这两种声明的作用范围不同,不能互相替代。

nonlocal:修改外围函数中的变量

nonlocal 用于在嵌套函数中声明:某个名称绑定在外围的函数作用域中,当前函数需要使用这个已有绑定,而不是创建同名的局部绑定。

例如:

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

def inner():
nonlocal count
count = count + 1
print(count)

inner()
print(count)


outer()

输出结果:

1
2
11
11

执行过程如下:

  1. 调用 outer(),创建局部变量 count,值为 10。
  2. 调用内部函数 inner()。
  3. nonlocal count 声明要使用外围函数中的 count 绑定。
  4. count = count + 1 将这个绑定对应的值更新为 11。
  5. 内部函数和外部函数随后读取到的都是更新后的值。

可以将这个过程理解为:

flowchart LR
    A["调用 outer()"] --> B["创建 count = 10"]
    B --> C["调用 inner()"]
    C --> D["声明 nonlocal count"]
    D --> E["将外围函数中的 count 更新为 11"]
    E --> F["inner() 输出 11"]
    F --> G["outer() 输出 11"]

这里还要注意:nonlocal 必须对应某个外围函数作用域中已经存在的绑定,不能用它来声明一个全局变量。如果要修改模块级名称,应使用 global。

另外,修改对象与重新绑定名称也不同。

例如:

1
2
3
4
5
6
7
8
9
10
11
def outer():
servers = []

def inner():
servers.append("web-01")

inner()
print(servers)


outer()

输出结果:

1
['web-01']

这里不需要 nonlocal。因为 servers.append() 修改的是列表对象本身,并没有让名称 servers 重新绑定到另一个对象。

但如果改成:

1
2
3
4
5
6
7
8
9
10
11
12
def outer():
servers = []

def inner():
nonlocal servers
servers = ["web-01"]

inner()
print(servers)


outer()

此时,servers 被重新绑定到一个新的列表对象,因此需要 nonlocal。

掌握这个区别,后面理解闭包和嵌套函数就容易多了。

闭包:为什么外部函数执行完,内部函数还能使用变量

回到最开始的例子:

1
2
3
4
5
6
7
8
9
def outer(x):
def inner(y):
return x + y

return inner


f = outer(10)
print(f(5))

当 outer(10) 执行结束后,内部函数 inner() 仍然可以访问 x,并不是因为 Python 将 x 变成了全局变量,而是因为返回的函数对象保留了访问相关外围作用域绑定的能力。

这种函数与其所需的外围作用域绑定共同构成的机制,称为闭包(closure)。

闭包常见于以下场景:

  • 创建具有不同配置的函数。
  • 保存函数需要使用的状态。
  • 将公共逻辑封装起来,避免每次调用都重复传入相同的配置参数。

通过实际运行观察闭包

理解概念还不够,我们可以直接查看函数对象的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
def make_checker(threshold):
def check(cpu_usage):
return cpu_usage > threshold

return check


check_cpu = make_checker(80)

print(check_cpu(75))
print(check_cpu(90))
print(check_cpu.__code__.co_freevars)
print(check_cpu.__closure__[0].cell_contents)

输出结果:

1
2
3
4
False
True
('threshold',)
80

逐行解释。

首先:

1
check_cpu = make_checker(80)

创建了一个 CPU 检查函数,它使用的阈值为 80。

然后:

1
2
print(check_cpu(75))
print(check_cpu(90))

分别检查 CPU 使用率为 75 和 90 的情况。由于判断条件是 cpu_usage > threshold,因此前者为 False,后者为 True。

接下来,我们观察两个属性。

__code__.co_freevars

1
print(check_cpu.__code__.co_freevars)

输出:

1
('threshold',)

co_freevars 会列出函数代码中引用的自由变量名称。这里的 threshold 不是 check() 自己的局部参数,而是来自外围函数的作用域。

__closure__

1
print(check_cpu.__closure__[0].cell_contents)

输出:

1
80

对于这个例子,__closure__ 中保存了闭包单元,cell_contents 可以让我们观察单元中当前关联的值。

这就提供了一个可以实际验证的证据:返回的函数不仅能继续执行,而且确实保留了访问外围函数变量的能力。

需要注意的是,__closure__ 可能为 None,例如某个函数没有需要通过闭包单元保存的自由变量时,就不能直接访问 __closure__[0]。上面的访问方式适用于这个已经确认具有闭包的示例。

嵌套函数的运维实战:创建不同阈值的检查器

了解闭包后,我们来做一个更贴近 Linux 运维的例子。

假设你正在编写服务器巡检脚本,需要检查不同服务器的 CPU 使用率:

  • 普通业务服务器:CPU 使用率超过 80% 时告警。
  • 核心业务服务器:CPU 使用率超过 90% 时告警。

如果每次检查都传入阈值,当然也能实现。但当检查逻辑逐渐增多时,我们可以把阈值封装在一个检查器中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def make_cpu_checker(threshold):
def check(server_name, cpu_usage):
if cpu_usage > threshold:
print(
f"{server_name}: CPU 使用率 {cpu_usage}% "
f"超过阈值 {threshold}%,需要关注"
)
else:
print(
f"{server_name}: CPU 使用率 {cpu_usage}%,运行正常"
)

return check


normal_checker = make_cpu_checker(80)
critical_checker = make_cpu_checker(90)

normal_checker("web-01", 85)
critical_checker("db-01", 85)
critical_checker("db-02", 95)

输出结果:

1
2
3
web-01: CPU 使用率 85% 超过阈值 80%,需要关注
db-01: CPU 使用率 85%,运行正常
db-02: CPU 使用率 95% 超过阈值 90%,需要关注

这个例子有几个值得学习的地方。

第一,公共逻辑只需要编写一次。

检查 CPU 使用率、比较阈值、输出结果,这些逻辑都集中在 check() 中,不需要为普通服务器和核心服务器分别复制一份代码。

第二,不同的检查器拥有不同的配置。

1
2
normal_checker = make_cpu_checker(80)
critical_checker = make_cpu_checker(90)

这两次调用分别创建了不同的函数对象,它们使用不同的 threshold 绑定。

因此,虽然检查逻辑相同,但配置可以不同。

第三,调用检查器时不需要反复传入阈值。

1
normal_checker("web-01", 85)

调用方只需要提供服务器名称和 CPU 使用率,阈值已经由创建检查器时的参数确定。

这就是闭包在运维脚本中的一个实用价值:将公共逻辑与特定配置组合起来,生成可以重复使用的函数。

当然,生产环境中的 CPU 使用率通常还需要考虑采样周期、告警持续时间、数据来源以及误报处理。这里先聚焦函数封装和闭包,不把完整监控系统的复杂性混进示例中。

lambda 表达式:简洁地创建小型函数

嵌套函数解决了函数封装和作用域的问题。接下来,我们看看另一种创建函数的方式:lambda 表达式。

普通函数通常使用 def 定义:

1
2
3
4
5
def add(a, b):
return a + b


print(add(2, 3))

输出结果:

1
5

如果函数逻辑很简单,只是将两个参数相加,也可以写成:

1
2
3
add = lambda a, b: a + b

print(add(2, 3))

输出结果同样是:

1
5

lambda 的基本语法是:

1
lambda 参数: 表达式

它会创建一个函数对象,并将表达式的计算结果作为返回值。

例如:

1
2
3
square = lambda x: x * x

print(square(5))

输出结果:

1
25

这里的 lambda x: x * x 相当于一个接收参数 x、返回 x * x 的小型函数。

不过,lambda 只能包含一个表达式,不能像普通 def 函数那样直接编写多条语句。因此,它适合简单、短小的逻辑;如果函数包含复杂判断、异常处理或多步操作,通常还是使用 def 更清晰。

lambda 的实际用途:配合 map、filter 和 sorted

上一篇文章已经学习了 map() 和 filter()。现在,我们可以把 lambda 用到这些函数中。

假设有一组服务器 CPU 使用率:

1
cpu_usage = [35, 82, 67, 91, 48]

使用 map() 批量处理数据

如果希望将每个 CPU 使用率转换为对应的剩余百分比,可以这样写:

1
2
3
4
5
cpu_usage = [35, 82, 67, 91, 48]

remaining = list(map(lambda usage: 100 - usage, cpu_usage))

print(remaining)

输出结果:

1
[65, 18, 33, 9, 52]

lambda usage: 100 - usage 定义了转换规则,map() 将这条规则应用到每个元素上,最后使用 list() 将结果转换为列表。

这里的“剩余百分比”只是演示数据转换的示例,并不意味着 CPU 使用率的补数一定等于真实系统中的 CPU 空闲率。实际监控数据还要考虑指标定义和采样口径。

使用 filter() 筛选数据

如果希望找出 CPU 使用率超过 80% 的记录:

1
2
3
4
5
cpu_usage = [35, 82, 67, 91, 48]

high_usage = list(filter(lambda usage: usage > 80, cpu_usage))

print(high_usage)

输出结果:

1
[82, 91]

这里,lambda usage: usage > 80 返回布尔值。filter() 保留判断结果为真的元素,因此最终得到 [82, 91]。

使用 sorted() 排序

再来看一个运维中常见的需求:对服务器的 CPU 使用率从高到低排序。

1
2
3
4
5
6
7
8
9
10
11
12
13
servers = [
{"name": "web-01", "cpu": 75},
{"name": "web-02", "cpu": 92},
{"name": "db-01", "cpu": 68},
]

result = sorted(
servers,
key=lambda server: server["cpu"],
reverse=True
)

print(result)

输出结果:

1
[{'name': 'web-02', 'cpu': 92}, {'name': 'web-01', 'cpu': 75}, {'name': 'db-01', 'cpu': 68}]

其中:

1
key=lambda server: server["cpu"]

表示使用每条服务器记录中的 cpu 字段作为排序依据。

reverse=True 表示按降序排列,因此 CPU 使用率最高的服务器排在最前面。

这就是 lambda 最常见的用法之一:当某个函数需要接收一个简单的处理规则时,可以用 lambda 快速提供这个规则,而不必专门定义一个只使用一次的函数。

嵌套函数、闭包和 lambda 有什么区别

概念主要用途典型场景
嵌套函数在函数内部定义另一个函数封装内部逻辑
闭包让函数继续访问相关的外围作用域绑定创建带有独立配置的检查器
nonlocal在嵌套函数中重新绑定外围函数的名称实现计数器或更新内部状态
lambda简洁地创建只有一个表达式的函数排序、映射和筛选

这几个概念可以组合使用,但它们并不是同一回事。

例如,下面的代码使用 lambda 创建了一个闭包:

1
2
3
4
5
6
7
8
9
def make_multiplier(factor):
return lambda number: number * factor


double = make_multiplier(2)
triple = make_multiplier(3)

print(double(5))
print(triple(5))

输出结果:

1
2
10
15

make_multiplier() 返回的 lambda 函数会访问外围函数中的 factor。因此,double 和 triple 虽然使用相同的函数逻辑,却分别关联了不同的乘数。

这说明 lambda 也可以形成闭包。闭包的关键在于函数对外围作用域绑定的访问能力,而不是函数究竟使用 def 还是 lambda 定义。

实际开发中应该怎么选择

学习这些语法,不是为了把代码写得越短越好,而是为了让程序更容易理解、维护和扩展。

可以记住几个简单的原则:

  • 需要封装内部逻辑时,考虑使用嵌套函数。
  • 需要让函数保留特定配置时,考虑使用闭包。
  • 需要在嵌套函数中重新绑定外围函数的变量时,使用 nonlocal。
  • 只需要提供一条简单的表达式规则时,可以考虑使用 lambda。
  • 函数逻辑较复杂,或者需要清晰的函数名时,优先使用 def。

例如,在服务器巡检脚本中,如果多个检查任务使用相同的判断逻辑、但阈值不同,可以用闭包生成不同的检查器。如果只是对服务器列表进行排序,直接使用 key=lambda ... 就足够了。

不要为了使用嵌套函数或 lambda 而使用它们。代码的可读性和可维护性,比少写几行更重要。

写在最后

这篇文章,我们从嵌套函数出发,逐步理解了 Python 的作用域、LEGB 名称查找模型、global 与 nonlocal 的区别,以及闭包如何让函数继续访问外围作用域中的绑定。

同时,我们还通过 __closure__ 和 co_freevars 实际观察了闭包,并使用 map()、filter() 和 sorted() 练习了 lambda 表达式。

接下来,我们将学习另一个有趣的话题:递归函数。

当一个函数能够调用自身时,程序就可以用一种简洁的方式处理具有重复结构的问题。不过,递归也有自己的使用条件和限制。下一篇文章,我们再结合实际示例,把它讲清楚。