第2章 2.4 作用域与匿名函数
「学会了怎么定义函数,上一章的任务就完成了。但你可能会遇到一个让人头秃的问题——为什么函数外面的变量,函数里面读不到?函数里面的变量,出去后就没了?这就是今天要解决的『变量去哪儿了』的问题。」
「学完这章,你就能彻底搞懂变量的『寿命』和『活动范围』,以及 Python 里一个超好用的『一次性函数』——匿名函数,也叫 lambda。准备好了吗?」
🎯 开场 3 分钟:为什么要学这个?
真实场景:外卖小哥的困境
想象你点了个外卖:
- 外卖小哥(函数):从店里取餐,送到你门口
- 你家的门牌号(作用域):小哥只能在门口交接,不能进你客厅
- 你家的冰箱(全局变量):放在公共区域,谁都能拿
问题来了:如果你告诉小哥「帮我把我房间床头柜上的充电器一起带下来」,他会一脸懵——他根本进不去你的房间!
这就是作用域要解决的问题:每个函数只能访问它「该访问」的地方的变量。
两个痛点,你肯定遇到过
- 痛点 1\n\n
\n\n
\n\n:在函数里改了一个变量,发现外面的值没变——「我明明改了啊!」 - 痛点 2:想在一个函数里访问另一个函数的变量——「怎么访问不了?」
学完本文你能
- 搞清楚 Python 变量的「活动范围」,不再踩作用域的坑
- 学会用
lambda写一次性函数,让代码更简洁 - 理解「闭包」这个听起来高大上、其实很简单的概念
🧱 基础 25 分钟:核心概念
2.4.1 作用域:变量的「寿命范围」
什么是作用域?
类比:就好比每个城市都有自己的「政策范围」——你在北京交的社保,在上海用不了;你在函数里定义的变量,出了函数就用不了。
Python 有 3 种主要作用域:
| 作用域 | 关键词 | 生命周期 | 举个例子 |
|---|---|---|---|
| 局部作用域 | local |
函数内 | 你的房间 |
| 外层作用域 | enclosing |
外层函数内 | 客厅 |
| 全局作用域 | global |
整个文件 | 小区的公共设施 |
| 内置作用域 | builtin |
Python 预装 | 自来水公司 |
查找规则:Python 找变量时,遵循 L-E-G-B 原则(Local → Enclosing → Global → Builtin),找不到就报错。
为什么用作用域?
解决痛点:防止函数里的变量「污染」外面的世界,同时避免外面的变量被函数不小心改乱。
怎么用?
例子 1:局部变量,出了函数就没了
def buy_snacks():
snack = "薯片" # 这个变量只在函数里存在
print(f"买了 {snack}")
buy_snacks()
print(snack) # ❌ 报错!NameError: name 'snack' is not defined
运行结果:
File "test.py", line 6, in <module>
print(snack)
NameError: name 'snack' is not defined
解释:第 2 行定义的 snack 是「局部变量」,就像你房间里的东西,出了房间门就没了。
例子 2:全局变量,大家都能用
drinks = "可乐" # 全局变量,放在函数外面
def buy_drink():
print(f"买了 {drinks}") # ✅ 能读到全局变量
buy_drink()
print(f"外面也看到 {drinks}") # ✅ 外面也能用
运行结果:
买了 可乐
外面也看到 可乐
例子 3:在函数里改全局变量?小心踩坑!
count = 0 # 全局变量
def add_one():
count = count + 1 # ❌ 报错!UnboundLocalError
print(count)
add_one()
运行结果:
Traceback (most recent call last):
File "test.py", line 4, in add_one
count = count + 1
UnboundLocalError: local variable 'count' referenced before assignment
解释:Python 看到你在函数里有 count = ...,就认为 count 是局部变量。但 count + 1 在赋值之前执行,找不到值,所以报错。
正确写法:用 global 声明全局变量
count = 0 # 全局变量
def add_one():
global count # ✅ 声明我要用全局的 count
count = count + 1
print(count)
add_one()
print(f"外面看到 count = {count}")
运行结果:
1
外面看到 count = 1
⚠️ 注意:虽然 global 能用,但过度使用会讓代码变得混乱——函数和外面共享变量,出了 bug 很难找。一般不推荐用 global。
2.4.2 nonlocal:访问外层函数的变量
什么是 nonlocal?
类比:你住在你爸妈家(外层函数),你可以用客厅的东西(外层变量),但不能直接改房产证(全局变量)。
nonlocal 让你可以读写外层(非全局)函数的变量。
为什么要用 nonlocal?
解决痛点:在嵌套函数里,外层函数的变量「既不能直接改,又不能 global」,需要 nonlocal 来打报告。
怎么用?
def outer():
message = "Hello" # 外层函数的变量
def inner():
nonlocal message # ✅ 声明我要改外层的 message
message = "Hi" # 现在可以改了
inner()
print(f"外层看到: {message}") # 输出 "Hi"
outer()
运行结果:
外层看到: Hi
如果不用 nonlocal 会怎样?
def outer():
message = "Hello"
def inner():
message = "Hi" # 这是在 inner 里新建了一个局部变量
inner()
print(f"外层看到: {message}") # 还是 "Hello",没变!
outer()
运行结果:
外层看到: Hello
解释:没有 nonlocal,inner 里的 message = "Hi" 创建了一个新的局部变量,外层的 message 纹丝不动。
2.4.3 匿名函数 lambda:一次性函数
什么是匿名函数?
类比:就像你去奶茶店,不用给店员起名字,直接说「一杯奶茶」就行了。lambda 就是「不用起名字的函数」。
匿名函数:没有名字的函数,用 lambda 关键字定义,通常只用一次。
为什么要用 lambda?
解决痛点:
1. 写一个只用一次的函数,还要专门起名字,太麻烦
2. 把函数当参数传进去时,专门定义一个函数太啰嗦
常见场景:排序时自定义规则、map/filter/reduce、回调函数
怎么用?
lambda 的基本语法:
lambda 参数: 表达式
例子 1:最简单的 lambda
# 传统函数
def add(a, b):
return a + b
# 用 lambda 简化
add_lambda = lambda a, b: a + b
# 两种写法效果一样
print(add(1, 2)) # 3
print(add_lambda(1, 2)) # 3
例子 2:lambda 当参数传给 sorted(最常见用法)
students = [
{"name": "小明", "score": 85},
{"name": "小红", "score": 92},
{"name": "小李", "score": 78}
]
# 按分数从高到低排序
sorted_students = sorted(students, key=lambda x: x["score"], reverse=True)
for s in sorted_students:
print(f"{s['name']}: {s['score']}")
运行结果:
小红: 92
小明: 85
小李: 78
解释:
- sorted() 的 key 参数需要传入一个函数
- 用 lambda x: x["score"] 直接定义「怎么提取分数」,不用单独写个函数
- reverse=True 表示降序排列
例子 3:lambda 配合 map 使用
prices = [100, 250, 80, 320]
# 给每个价格打 8 折,四舍五入保留整数
discounted = list(map(lambda x: round(x * 0.8), prices))
print(f"原价: {prices}")
print(f"折后: {discounted}")
运行结果:
原价: [100, 250, 80, 320]
折后: [80, 200, 64, 256]
例子 4:lambda 配合 filter 使用
ages = [12, 18, 25, 16, 30, 22]
# 筛选出成年人的年龄
adults = list(filter(lambda x: x >= 18, ages))
print(f"所有人: {ages}")
print(f"成年人: {adults}")
运行结果:
所有人: [12, 18, 25, 16, 30, 22]
成年人: [18, 25, 30, 22]
2.4.4 闭包:函数带着「行李」出门
什么是闭包?
类比:你去旅游,行李箱里装了你从家里带的东西(外层函数的变量)。就算你回到家(函数执行完),行李箱里的东西还在——这个「带着行李出门」的特性就是闭包。
闭包:一个内层函数引用了外层函数的变量,内层函数带着这些变量「跑」到任何地方。
为什么要用闭包?
解决痛点:
1. 想让函数记住自己的「状态」,但又不想用类
2. 需要「工厂函数」——根据参数生成不同的函数
怎么用?
例子 1:闭包记住外层变量
def make_multiplier(factor):
"""工厂函数:生成乘法器"""
def multiply(x):
return x * factor # 引用了外层的 factor
return multiply # 返回内层函数,带着 factor 这个「行李」
# 生成了两个不同的乘法器
double = make_multiplier(2) # factor=2
triple = make_multiplier(3) # factor=3
print(double(5)) # 5 * 2 = 10
print(triple(5)) # 5 * 3 = 15
print(double(10)) # 10 * 2 = 20
运行结果:
10
15
20
解释:
- make_multiplier(2) 返回了 multiply 函数
- double 函数「记住」了 factor=2
- 每次调用 double(5),都带着 factor=2 去计算
例子 2:用闭包替代类
class Counter:
def __init__(self):
self.count = 0
def increment(self):
self.count += 1
return self.count
# 用闭包实现同样的效果
def make_counter():
count = 0
def increment():
nonlocal count
count += 1
return count
return increment
counter1 = make_counter()
counter2 = make_counter()
print(counter1()) # 1
print(counter1()) # 2
print(counter2()) # 1 (独立的计数器)
print(counter1()) # 3
运行结果:
1
2
1
3
解释:counter1 和 counter2 是两个独立的计数器,互不干扰——每个闭包有自己的「行李箱」(count 变量)。
🔥 实战 35 分钟:3 个递进的小项目
项目 1(5 分钟):用 lambda + sorted 排序成绩单
需求:给一个学生成绩列表,按平均分从高到低排序,输出排名。
# 学生成绩数据
students = [
{"name": "张三", "chinese": 85, "math": 92, "english": 78},
{"name": "李四", "chinese": 90, "math": 75, "english": 88},
{"name": "王五", "chinese": 72, "math": 95, "english": 85},
]
# 用 lambda 计算平均分,然后排序
sorted_students = sorted(
students,
key=lambda s: (s["chinese"] + s["math"] + s["english"]) / 3,
reverse=True
)
# 输出排名
print("=" * 30)
print(" 学生成绩排名(平均分)")
print("=" * 30)
for i, s in enumerate(sorted_students, 1):
avg = (s["chinese"] + s["math"] + s["english"]) / 3
print(f"第{i}名: {s['name']:4s} - 平均 {avg:.1f} 分")
预期输出:
==============================
生成绩排名(平均分)
==============================
第1名: 王五 - 平均 84.0 分
第1名: 张三 - 平均 85.0 分
第1名: 李四 - 平均 84.3 分
一句话解释:lambda s: (s["chinese"] + s["math"] + s["english"]) / 3 定义了「平均分怎么算」,sorted 靠它来排序。
项目 2(15 分钟):用闭包实现「折扣计算器工厂」
需求:创建一个折扣计算器,可以处理「满100减20」「打8折」「满200返50」等多种折扣方式。
def make_discount_calculator(discount_type, threshold=0, discount_amount=0, discount_rate=1.0):
"""
折扣计算器工厂
- discount_type: "满减" / "打折" / "返现"
- threshold: 满多少
- discount_amount: 减多少 / 返多少
- discount_rate: 折扣率
"""
def calculate(original_price):
if discount_type == "满减":
# 满 threshold 减 discount_amount
if original_price >= threshold:
return original_price - discount_amount
return original_price
elif discount_type == "打折":
# 打 discount_rate 折
return original_price * discount_rate
elif discount_type == "返现":
# 满 threshold 返 discount_amount
if original_price >= threshold:
return original_price - discount_amount
return original_price
return original_price # 默认不打折
return calculate
# 创建三种不同的折扣计算器
calculator1 = make_discount_calculator("满减", threshold=100, discount_amount=20)
calculator2 = make_discount_calculator("打折", discount_rate=0.8)
calculator3 = make_discount_calculator("返现", threshold=200, discount_amount=50)
# 测试价格
test_prices = [80, 150, 200, 350]
print("原始价格:", test_prices)
print("-" * 40)
print(f"满100减20: {[calculator1(p) for p in test_prices]}")
print(f"打8折: {[calculator2(p) for p in test_prices]}")
print(f"满200返50: {[calculator3(p) for p in test_prices]}")
预期输出:
原始价格: [80, 150, 200, 350]
----------------------------------------
满100减20: [80, 130, 180, 330]
打8折: [64.0, 120.0, 160.0, 280.0]
满200返50: [80, 150, 150, 300]
一句话解释:闭包让每个计算器「记住」自己的折扣规则,互不干扰,可以像普通函数一样调用。
项目 3(15 分钟):综合实战——待办清单管理器(用闭包保存状态)
需求:做一个命令行待办清单,支持添加任务、查看任务、完成任务,用闭包来「记住」任务列表。
def make_todo_manager():
"""待办清单管理器(用闭包保存状态)"""
tasks = [] # 外层函数的变量,被内层函数引用,形成闭包
def add(task):
tasks.append({"task": task, "done": False})
return f"✅ 已添加: {task}"
def list_tasks():
if not tasks:
return "📝 暂无任务"
result = []
for i, t in enumerate(tasks, 1):
status = "✓" if t["done"] else "○"
result.append(f"{i}. [{status}] {t['task']}")
return "\n".join(result)
def complete(index):
if 0 <= index < len(tasks):
tasks[index]["done"] = True
return f"🎉 已完成: {tasks[index]['task']}"
return "❌ 无效序号"
def delete(index):
if 0 <= index < len(tasks):
removed = tasks.pop(index)
return f"🗑️ 已删除: {removed['task']}"
return "❌ 无效序号"
# 返回一个包含所有方法的字典(Python 可以返回函数)
return {"add": add, "list": list_tasks, "complete": complete, "delete": delete}
# 使用待办清单
todo = make_todo_manager()
print("=== 待办清单测试 ===")
print(todo["add"]("完成 Python 作用域练习"))
print(todo["add"]("写一个爬虫脚本"))
print(todo["add"]("整理笔记"))
print()
print("当前任务列表:")
print(todo["list"]())
print()
print(todo["complete"](0)) # 完成第一个任务
print(todo["complete"](1)) # 完成第二个任务
print()
print("更新后的任务列表:")
print(todo["list"]())
预期输出:
=== 待办清单测试 ===
✅ 已添加: 完成 Python 作用域练习
✅ 已添加: 写一个爬虫脚本
✅ 已添加: 整理笔记
当前任务列表:
1. [○] 完成 Python 作用域练习
2. [○] 写一个爬虫脚本
3. [○] 整理笔记
🎉 已完成: 完成 Python 作用域练习
🎉 已完成: 写一个爬虫脚本
更新后的任务列表:
1. [✓] 完成 Python 作用域练习
2. [✓] 写一个爬虫脚本
3. [○] 整理笔记
一句话解释:闭包让 tasks 变量「活」在返回的函数里,每次调用 todo["add"]() 等方法,都能访问和修改同一个 tasks 列表。
💪 进阶 20 分钟:常见坑 + 性能小贴士
坑 1:lambda 捕获变量是「快照」还是「引用」?
❌ 错误示例:
funcs = []
for i in range(3):
funcs.append(lambda: i) # 所有人都记住了同一个 i
for f in funcs:
print(f()) # 输出 2, 2, 2 而不是 0, 1, 2
预期错误输出:
2
2
2
✅ 正确写法:
funcs = []
for i in range(3):
funcs.append(lambda i=i: i) # 用默认参数「快照」当前 i 的值
for f in funcs:
print(f()) # 输出 0, 1, 2
预期正确输出:
0
1
2
解释:lambda i=i: i 用 i 的当前值作为默认参数,相当于「拍照」;否则 lambda 捕获的是变量 i 的引用,而不是值。
坑 2:在循环里用 lambda + nonlocal,小心!
❌ 错误示例:
def make_counters():
counters = []
for i in range(3):
# ❌ 错误:所有闭包共享同一个 i
counters.append(lambda: i)
return counters
counters = make_counters()
print(counters[0]()) # 2 而不是 0
print(counters[1]()) # 2 而不是 1
print(counters[2]()) # 2
✅ 正确写法:
def make_counters():
counters = []
for i in range(3):
# ✅ 正确:用默认参数捕获当前 i
counters.append(lambda i=i: i)
return counters
counters = make_counters()
print(counters[0]()) # 0
print(counters[1]()) # 1
print(counters[2]()) # 2
坑 3:lambda 只能写表达式,不能写语句
❌ 错误示例:
# 尝试在 lambda 里写 if-else 语句(Python 的 if 是语句,不能这样用)
f = lambda x: if x > 0: "正数" else "非正数" # ❌ 语法错误!
✅ 正确写法:
# 用三元表达式
f = lambda x: "正数" if x > 0 else "非正数"
print(f(5)) # 正数
print(f(-3)) # 非正数
坑 4:global 变量在函数里不声明就改,会报错
❌ 错误示例:
balance = 1000
def withdraw(amount):
balance = balance - amount # ❌ UnboundLocalError
return balance
✅ 正确写法:
balance = 1000
def withdraw(amount):
global balance # ✅ 声明要用全局变量
balance = balance - amount
return balance
print(withdraw(200)) # 800
但更好的写法是返回新值:
def withdraw(balance, amount):
return balance - amount # 不改原值,返回新值
balance = 1000
balance = withdraw(balance, 200) # 用新值覆盖
print(balance) # 800
坑 5:闭包 vs 类——什么时候用哪个?
| 场景 | 推荐 | 原因 |
|---|---|---|
| 简单状态(计数器、缓存) | 闭包 | 代码更简洁 |
| 复杂状态 + 多种行为 | 类 | 更清晰易维护 |
| 需要继承、类型提示 | 类 | 类更适合 OOP |
小贴士:如果你的「类」只有一个方法,考虑用闭包替代。
性能小优化:lambda vs 普通函数,差别微乎其微
import timeit
# lambda 版本
t1 = timeit.timeit('list(map(lambda x: x*2, range(1000)))', number=1000)
# 普通函数版本
def double(x): return x*2
t2 = timeit.timeit('list(map(double, range(1000)))', number=1000)
print(f"lambda: {t1:.4f}s")
print(f"普通函数: {t2:.4f}s")
结论:性能差异在毫秒级,可以忽略不计。选择 lambda 还是 def,取决于代码可读性——用一次且简单用 lambda,多次使用用 def。
调试技巧:用 __closure__ 查看闭包
def outer(x):
def inner(y):
return x + y
return inner
f = outer(10)
print(f.__closure__) # 查看闭包包含的变量
print(f.__closure__[0].cell_contents) # 查看具体的值
输出:
(<cell at 0x...>,)
10
✏️ 练习题 + 作业题
练习题(5 道,10 分钟内完成)
练习 1(2 分钟):global 声明
- 输入:有一个全局变量 name = "小明",写一个函数把它改成 "小红"
- 预期输出:name = 小红
- 提示:用到 global 关键字
练习 2(2 分钟):lambda 排序
- 输入:列表 [3, 1, 4, 1, 5, 9],用 sorted + lambda 按绝对值与 4 的差排序
- 预期输出:[3, 1, 4, 5, 1, 9](|3-4|=1, |1-4|=3, |4-4|=0, |5-4|=1, |9-4|=5...)
- 提示:key=lambda x: abs(x-4)
练习 3(2 分钟):nonlocal 陷阱
- 输入:下面代码为什么会输出 100 而不是 50?该怎么改?
def outer():
x = 100
def inner():
x = 50 # 猜猜输出什么?
inner()
print(x)
outer()
- 预期输出:解释为什么是 100,并给出修改方案
- 提示:用
nonlocal声明
练习 4(2 分钟):闭包计数器
- 输入:写一个函数 make_counter(),每次调用返回的函数都会比上次多 1
- 预期输出:
c1 = make_counter()
print(c1()) # 1
print(c1()) # 2
c2 = make_counter()
print(c2()) # 1(独立的计数器)
print(c1()) # 3
- 提示:用
nonlocal在闭包里更新计数器
练习 5(2 分钟):修复 lambda 陷阱
- 输入:下面代码为什么都输出 3?怎么改?
funcs = [lambda: i for i in range(3)]
print(funcs[0]()) # 3
print(funcs[1]()) # 3
print(funcs[2]()) # 3
- 预期输出:
0, 1, 2 - 提示:用默认参数捕获循环变量的值
作业题(30 分钟-2 小时)
作业:做一个「折扣叠加计算器」
需求描述:
你是电商平台的程序员,需要实现一个折扣计算器,支持:
1. 添加多个折扣(满减、打折可以叠加)
2. 计算最终价格
3. 查看折扣明细
功能点:
1. add_discount(type, ...) - 添加折扣,支持"满100减20"、"打8折"、"满200返50"
2. calculate(price) - 计算最终价格(考虑折扣叠加顺序:先满减,后打折)
3. show_discounts() - 显示已添加的折扣列表
加分项:
1. 支持「指定折扣顺序」(如先打折后满减)
2. 支持「封顶」(如最多优惠 100 元)
验收标准:
- 能跑起来
- 正确计算:原价 350 元,满100减20 + 打8折 = ?
- 代码有注释,解释作用域和闭包的使用
📚 总结 + 资源
一句话总结本文学到的 3 个核心点
- 作用域:Python 用 L-E-G-B 规则找变量,函数内的变量「出不去」,外面的变量要
global才能改 - lambda:一次性函数,用
lambda x: 表达式定义,常配合sorted、map、filter使用 - 闭包:内层函数「带着」外层函数的变量跑,让函数记住自己的状态,比类更轻量
推荐延伸学习资源
- 官方文档:Python 作用域详解 - 权威且详细
- 书籍:《Python 编程:从入门到实践》- 第 9 章「类和迭代器」部分对闭包有很好解释
- 视频:B站 up 主「小甲鱼」的 Python 进阶教程第 38-40 集
互动钩子
「你在实际工作中用过闭包吗?遇到过什么有趣的『变量找不到』的 bug?评论区聊聊,老粉优先回复!
下一章我们要做一个超实用的综合项目——BMI 计算器 + 猜数字游戏,把函数、作用域、参数全部串起来用。剧透一下:猜数字游戏里,闭包可以帮助我们『记住』猜测次数哦!敬请期待~」

评论(0)