第2章 2.4 作用域与匿名函数

「学会了怎么定义函数,上一章的任务就完成了。但你可能会遇到一个让人头秃的问题——为什么函数外面的变量,函数里面读不到?函数里面的变量,出去后就没了?这就是今天要解决的『变量去哪儿了』的问题。」

「学完这章,你就能彻底搞懂变量的『寿命』和『活动范围』,以及 Python 里一个超好用的『一次性函数』——匿名函数,也叫 lambda。准备好了吗?」


🎯 开场 3 分钟:为什么要学这个?

真实场景:外卖小哥的困境

想象你点了个外卖:

  1. 外卖小哥(函数):从店里取餐,送到你门口
  2. 你家的门牌号(作用域):小哥只能在门口交接,不能进你客厅
  3. 你家的冰箱(全局变量):放在公共区域,谁都能拿

问题来了:如果你告诉小哥「帮我把我房间床头柜上的充电器一起带下来」,他会一脸懵——他根本进不去你的房间!

这就是作用域要解决的问题:每个函数只能访问它「该访问」的地方的变量。

两个痛点,你肯定遇到过

  • 痛点 1\n\nSimple tech illustration expla\n\nAI comic creation scene, creat\n\n:在函数里改了一个变量,发现外面的值没变——「我明明改了啊!」
  • 痛点 2:想在一个函数里访问另一个函数的变量——「怎么访问不了?」

学完本文你能

  1. 搞清楚 Python 变量的「活动范围」,不再踩作用域的坑
  2. 学会用 lambda 写一次性函数,让代码更简洁
  3. 理解「闭包」这个听起来高大上、其实很简单的概念

🧱 基础 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

解释:没有 nonlocalinner 里的 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

解释counter1counter2 是两个独立的计数器,互不干扰——每个闭包有自己的「行李箱」(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: ii 的当前值作为默认参数,相当于「拍照」;否则 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 个核心点

  1. 作用域:Python 用 L-E-G-B 规则找变量,函数内的变量「出不去」,外面的变量要 global 才能改
  2. lambda:一次性函数,用 lambda x: 表达式 定义,常配合 sortedmapfilter 使用
  3. 闭包:内层函数「带着」外层函数的变量跑,让函数记住自己的状态,比类更轻量

推荐延伸学习资源

  1. 官方文档Python 作用域详解 - 权威且详细
  2. 书籍:《Python 编程:从入门到实践》- 第 9 章「类和迭代器」部分对闭包有很好解释
  3. 视频:B站 up 主「小甲鱼」的 Python 进阶教程第 38-40 集

互动钩子

「你在实际工作中用过闭包吗?遇到过什么有趣的『变量找不到』的 bug?评论区聊聊,老粉优先回复!

下一章我们要做一个超实用的综合项目——BMI 计算器 + 猜数字游戏,把函数、作用域、参数全部串起来用。剧透一下:猜数字游戏里,闭包可以帮助我们『记住』猜测次数哦!敬请期待~」

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。