=分割线=
这个好难啊。。。。。
一、核心概念:什么是「返回函数」?
1. 基础理解
正常函数:传入参数,返回数据结果(数字、字符串、列表等)
返回函数:传入参数,返回另一个函数,不立刻计算结果,需要的时候再调用函数计算。
2. 设计思路
有些场景不需要马上算出结果,想延迟计算,先保存好参数,后续要用再执行计算,这时候就用「返回函数」。
二、基础实例:延迟求和(看懂最基础用法)
1. 普通立刻求和
# 调用函数直接算出结果
def calc_sum(*args):
ax = 0
for n in args:
ax += n
return ax
# 直接得到答案
print(calc_sum(1,3,5,7,9)) # 25
2. 返回函数|延迟求和
# 外层函数:接收参数,返回内部求和函数
def lazy_sum(*args):
# 内层函数:真正做求和运算
def sum():
ax = 0
for n in args:
ax += n
return ax
# 重点:返回函数本身,不是返回计算结果
return sum
执行流程拆解
-
f = lazy_sum(1,3,5,7,9)只
保存参数
,不计算,返回一个求和函数,赋值给变量
f;
-
f()加括号调用函数,才会真正循环求和,算出结果。
运行演示
f = lazy_sum(1, 3, 5, 7, 9)
print(f) # 输出:函数对象,没计算
print(f()) # 25 调用后才出结果
三、重点核心:闭包(Closure)
1. 通俗定义
内层函数 引用 外层函数的变量 / 参数;
外层函数结束后,外层的变量不会销毁,会被内层函数牢牢保存,这个结构就叫闭包。
2. 闭包两大特点
- 每次调用外层函数,都会生成全新独立的内层函数,互不干扰;
- 内层函数延后执行,会沿用外层保存的变量。
示例:
f1 = lazy_sum(1,3,5,7,9)
f2 = lazy_sum(1,3,5,7,9)
print(f1 == f2) # False 两个完全不同的函数
四、闭包经典坑:循环变量陷阱(必看)
1. 错误代码 & 问题
def count():
fs = []
for i in range(1, 4): # i会依次变成1、2、3
def f():
return i * i
fs.append(f) #此处注意!f不是f(),不进行计算
return fs
f1, f2, f3 = count()
print(f1()) # 9
print(f2()) # 9
print(f3()) # 9
通俗原因
- 循环时只创建函数,不执行函数;
- 所有函数都引用同一个循环变量
i; - 循环结束后,
i最终固定为3; - 后面调用
f1()/f2()/f3(),才去取值,全部用最后的3计算。
2. 正确解决办法
用一层额外函数,绑定循环变量的当前值,把瞬时值固定下来:
def count():
# 增加一层函数绑定变量
def f(j):
def g():
return j * j
return g
fs = []
for i in range(1, 4):
fs.append(f(i)) # 立刻传入当前i的值,固定保存
return fs
f1, f2, f3 = count()
print(f1()) # 1
print(f2()) # 4
print(f3()) # 9
✅ 闭包黄金规则
返回的内层函数,千万不要引用循环变量、会变化的变量,容易出 bug。
五、nonlocal 关键字(修改外层变量必备)
1. 场景
闭包内层函数:
- 只读外层变量:直接用,没问题;
- 修改、赋值外层变量:会报错,Python 会把它当成局部变量。
2. 报错示例
def inc():
x = 0
def fn():
x = x + 1 # 报错!分不清是局部还是外层变量
return x
return fn
3. 正确用法:加 nonlocal
def inc():
x = 0
def fn():
nonlocal x # 声明:x不是我本地变量,是外层函数的变量
x = x + 1
return x
return fn
f = inc()
print(f()) # 1
print(f()) # 2
记忆口诀
内层想改外层变量,先写 nonlocal 变量名。
六、课后练习:闭包实现计数器(标准答案 + 详解)
需求:写一个计数器,每次调用数字 + 1,多个计数器互相独立。
def createCounter():
# 外层定义计数变量
num = 0
def counter():
# 声明修改外层变量
nonlocal num
num += 1
return num
return counter
# 测试
counterA = createCounter()
print(counterA(), counterA(), counterA(), counterA(), counterA())
counterB = createCounter()
if [counterB(), counterB(), counterB(), counterB()] == [1, 2, 3, 4]:
print('测试通过!')
else:
print('测试失败!')
思路拆解
- 外层函数
createCounter定义初始值num = 0; - 内层
counter通过nonlocal修改外层num; - 每次调用内层函数,数字自增 1;
- 不同计数器(counterA/counterB)互相独立,互不影响。
七、全篇知识小结
- 返回函数:函数可以返回另一个函数,实现延迟计算;
- 闭包:内层函数保存外层函数的变量,生命周期延长;
- 避坑:闭包不要引用会变化的循环变量;
- nonlocal:内层函数修改外层局部变量的专属关键字;
- 实用场景:计数器、延迟计算、缓存数据等。
=分割线=理解
通俗类比
想象你有一个录音机(g),它录下的声音来自"当前环境的声音"(i)。如果所有录音机都指向同一个环境,那么环境改变后,所有录音机播放的都是最后的声音。
正确写法相当于:你在每次录音时,先走进一个隔音小房间(f),把小房间里的固定声音(j)录下来。这样每个录音机都拥有独立的声音。
形参是什么
形参 是"形式参数"的简称,指定义函数时括号里写的变量名,它代表函数将来接收的值。
通俗理解
- 形参 = 函数定义时的"占位符",相当于一个空盒子,等着别人往里装东西。
- 实参 = 调用函数时实际传入的值,相当于往盒子里装的具体东西。
问题本质(一句话)
循环中定义的函数,如果使用了循环变量,所有函数共享同一个变量,最后该变量的值是循环结束时的值。
常见混乱点澄清
f(i)立即执行,得到g;fs.append(g)是把g存起来,不是立即调用g。return g返回的是函数对象本身,不是结果。所以fs里放的是函数,不是数字。- 为什么不能用更简单的方式? 直接
def g(): return i*i会导致所有g共享同一个i(循环结束后i=3),结果全是 9。
如果你还是混乱,画"盒子"
想象每次调用 f(i) 都打开一个新盒子,盒子里有一个 j(就是传入的 i)。盒子里还放了一张纸条,上面写着"计算当前盒子里的 j * j"。
每次循环你得到一个盒子,并把盒子放进 fs。后来你打开盒子(调用函数),纸条就按盒子里的 j 计算。
=分割线=闭包理解
1. 先忘掉闭包,记住一个现象
python
def test():
x = 1
def inner():
print(x) # inner 函数用了外层的 x
return inner
func = test()
func() # 打印 1
这里 inner 就是闭包。闭包的本质就是:一个函数记住了它外部函数的变量,即使外部函数已经执行完毕。
2. 为什么需要闭包?
有时候你想创建一个"会记住某个值的函数",而不是传参数。
比如你想生成三个函数:第一个永远返回1,第二个永远返回
钱4,第三个永远返回9。
你可以这样写:
python
def make_square(n):
def f():
return n * n
return f
func1 = make_square(1)
func2 = make_square(2)
func3 = make_square(3)
print(func1()) # 1
print(func2()) # 4
print(func3()) # 9
这里 make_square 每调用一次,就新建一个函数 f,并且这个 f 记住了当时传入的 n。这就是闭包的典型应用。
3. 你最难理解的循环问题
python
fs = []
for i in range(1,4):
def f():
return i * i
fs.append(f)
为什么这里三个函数都返回9?因为 f 里面的 i 是引用 ,不是复制。当循环结束后,i 变成了3,所有 f 去取 i 时都取到3。
解决方案 :让每个 f 在创建时就"固定"住当前的 i,而不是等到调用时再去取。
方法就是再套一层函数,把当前 i 作为参数传进去:
python
fs = []
for i in range(1,4):
def outer(x): # x 接收当前的 i
def inner():
return x * x
return inner
fs.append(outer(i)) # 调用 outer(i) 立刻执行,生成inner并返回
或者更简单的默认参数法(推荐):
python
fs = []
for i in range(1,4):
def f(i=i):
return i * i
fs.append(f)
4. 你的困惑总结
- 你学会了
fs.append(f)不加括号是存函数,不是调函数。✅ - 你理解了循环后
i=3。✅ - 你不理解为什么还要多一层
def outer(x)。因为这层的作用是把当前的i值作为参数传入 ,从而在inner的作用域里保存一份独立的副本 ,而不是共享外层的i。
如果你觉得这个三层写法太绕,那就不必强行用它。在实际写代码时,你完全可以只用默认参数的方式,同样能实现闭包效果,且更容易理解。
你的目标不是学会三种写法,而是理解"为什么需要额外手段来固定循环变量的值"。你现在已经知道这个原因了,那就够了。