Day17 · 函数(二)递归、作用域、闭包与 lambda
Day17 · 函数(二)递归、作用域、闭包与 lambda
阶段:阶段一 · 核心基础 | 前置:Day16 | 技术基准:Python 3.15.0
学习目标
- 理解递归的两个必要条件,会写阶乘、斐波那契等递归函数,知道递归深度限制
- 理解尾递归概念,以及"Python 不做尾递归优化"这个重要事实
- 掌握变量作用域:LEGB 查找规则、
global、nonlocal、嵌套作用域 - 理解闭包(Closure),会写计数器闭包,警惕闭包的"延迟绑定"陷阱
- 会用 lambda 匿名函数配合
sorted/map/filter,了解functools.partial偏函数
一、递归(Recursion)
1.1、什么是递归
递归就是"函数自己调用自己"。它把一个大问题层层拆成规模更小的同类问题,直到碰到一个"不用再拆、直接能给出答案"的出口(终止条件)。写递归必须满足两点:
- 明确的终止条件(base case)——否则会无限递归直到崩溃。
- 每次调用都在向终止条件逼近(参数在变小/变化)。
1.2、经典例子:累加与阶乘
# 计算 1 + 2 + ... + n
def sum_num(n):
if n == 1: # 终止条件
return 1
return n + sum_num(n - 1) # 自己调用自己,参数变小
print(sum_num(100)) # 输出:5050
# 计算 n 的阶乘 n! = 1*2*...*n
def factorial(n):
if n <= 1: # 0! 和 1! 都等于 1
return 1
return n * factorial(n - 1)
print(factorial(5)) # 输出:120
1.3、斐波那契数列
斐波那契数列:1, 1, 2, 3, 5, 8, 13 ...,每一项等于前两项之和:
def fib(n):
if n <= 2: # 第 1、2 项都是 1
return 1
return fib(n - 1) + fib(n - 2)
print([fib(i) for i in range(1, 11)])
# 输出:[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
小嘉提醒:上面这个朴素斐波那契有个性能问题——
fib(n-1)和fib(n-2)会重复计算大量相同的分支。n 稍大(比如 40)就会明显变慢。实际项目里可以用"记忆化缓存"或直接改成循环,这里先把递归逻辑本身搞懂。
1.4、递归深度与栈溢出
计算机里函数调用靠栈(stack)维护,每调一次就压入一个栈帧。栈大小是有限的,递归太深就会报 RecursionError。可以用 sys.getrecursionlimit() 查看上限(默认约 1000):
import sys
print(sys.getrecursionlimit()) # 输出类似:1000
def infinite(n):
return infinite(n + 1) # 没有终止条件
# infinite(0) # 取消注释会报 RecursionError: maximum recursion depth exceeded
1.5、尾递归(Tail Recursion)——以及 Python 不优化它
尾递归指:把"中间计算结果"通过参数一路带下去,函数在 return 时只调用自身、不再做乘法之类的额外表达式。理论上编译器可以把尾递归优化成"只占用一个栈帧":
# 普通递归:每层都要保留栈帧去算 n * fact(n-1)
def fact(n):
if n == 1:
return 1
return n * fact(n - 1)
# 尾递归写法:把累计结果 acc 通过参数传下去
def fact_tail(n, acc=1):
if n == 1:
return acc
return fact_tail(n - 1, acc * n)
print(fact(5)) # 输出:120
print(fact_tail(5)) # 输出:120
但是——Python 解释器并没有做尾递归优化,上面的 fact_tail 递归深了一样会栈溢出。所以在 Python 里,深递归问题更推荐直接用循环或后面会讲的生成器来解决,不要迷信尾递归。
二、变量作用域(Scope)
2.1、局部变量与全局变量
定义在函数外面的变量是全局变量,定义在函数内部的是局部变量:
num = 100 # 全局变量
def test():
num = 200 # 这是一个同名的"新"局部变量,跟外面的无关
print("函数内:", num)
test() # 输出:函数内: 200
print("函数外:", num) # 输出:函数外: 100(外面没被改)
函数内部可以读取全局变量,但如果只是想"看",直接读就行;一旦在函数里给同名变量赋值,Python 就会把它当成一个新的局部变量。
2.2、LEGB 查找规则
当你用到一个名字时,Python 按以下顺序从内到外查找它,找到第一个就停:
| 顺序 | 名称 | 含义 |
|---|---|---|
| L | Local | 当前函数内部 |
| E | Enclosing | 外层嵌套函数 |
| G | Global | 当前模块(文件)的全局变量 |
| B | Builtin | Python 内置名字(如 len、print) |
x = "全局"
def outer():
x = "外层函数"
def inner():
x = "内层函数"
print(x) # 输出:内层函数(先找 Local)
inner()
outer()
2.3、global 关键字
如果确实想在函数里修改全局变量,需要用 global 声明:
num = 10
def change():
global num # 声明:这里的 num 就是全局那个
num = 20
change()
print(num) # 输出:20(全局变量被改了)
小嘉提醒:实际工程里要少用全局变量(违反"高内聚、低耦合")。全局变量一般当"常量"用;确实要改时才用
global。
2.4、nonlocal 与嵌套作用域
在嵌套函数里,想修改外层函数的变量(而不是全局变量),用 nonlocal:
def counter():
count = 0
def inner():
nonlocal count # 声明:用的是外层函数的 count
count += 1
return count
return inner
c = counter()
print(c()) # 输出:1
print(c()) # 输出:2
print(c()) # 输出:3
如果不加 nonlocal,count += 1 会被当成在 inner 里定义局部变量,从而报 UnboundLocalError: local variable 'count' referenced before assignment。
2.5、globals() 与 locals()
两个内置函数可以把全局/当前局部名字空间看成字典:
num = 10
name = "小嘉"
def test():
age = 18
print("局部:", locals()) # 输出:局部: {'age': 18}
print("全局变量个数:", len(globals()))
test()
print(globals()["num"]) # 输出:10
print(globals()["name"]) # 输出:小嘉
三、闭包(Closure)
3.1、什么是闭包
闭包指一种函数嵌套结构:内部函数引用了外部函数的变量,并且外部函数把内部函数"返回"出去。这样一来,外部函数即使已经执行结束,它的变量也能被返回出去的内部函数"记住"、持续使用。
def outer(x):
def inner(y):
return x + y # inner 用到了 outer 的变量 x
return inner # 返回 inner 这个函数本身(注意没加括号)
add5 = outer(5) # x 被"记住"为 5
print(add5(3)) # 输出:8 (5 + 3)
print(add5(10)) # 输出:15 (5 + 10)
add100 = outer(100)
print(add100(1)) # 输出:101
闭包让我们可以"不定义类"就做出带状态的函数,上面的 counter() 就是闭包的典型应用。
3.2、闭包的延迟绑定陷阱
闭包引用的是变量本身,不是"当时的值"。如果在循环里创建多个闭包,它们可能共享同一个变量、最后都取到循环结束后的值:
# 错误示范:循环里造 lambda,都引用同一个 i
funcs = [lambda x: x + i for i in range(3)]
print([f(10) for f in funcs]) # 输出:[12, 12, 12](i 最后是 2)
解决办法是用默认参数在定义时把当前值"钉住":
# 用 i=i 在定义时固定当前的 i
funcs = [lambda x, i=i: x + i for i in range(3)]
print([f(10) for f in funcs]) # 输出:[10, 11, 12]
四、lambda 匿名函数
4.1、基本用法
lambda 用来写"一句话就能写完"的小函数,没有名字,所以叫匿名函数。语法:lambda 参数: 表达式:
add = lambda a, b: a + b
print(add(3, 5)) # 输出:8
它等价于:
def add(a, b):
return a + b
lambda 里只能写一个表达式,不要塞复杂逻辑。它最常见的用途是作为"一次性小函数"传给其他函数。
4.2、配合 sorted / map / filter
data = [3, 1, 4, 1, 5, 9, 2, 6]
# sorted 的 key:指定按什么排序
print(sorted(data, key=lambda x: -x)) # 输出:[9, 6, 5, 4, 3, 2, 1, 1]
# map:对每个元素做同样的变换
print(list(map(lambda x: x * 2, data))) # 输出:[6, 2, 8, 2, 10, 18, 4, 12]
# filter:按条件过滤
print(list(filter(lambda x: x > 4, data))) # 输出:[5, 9, 6]
一个更贴近实际的例子——按元组里的第 2 个元素排序:
students = [("小嘉", 90), ("阿明", 85), ("小红", 95)]
students.sort(key=lambda s: s[1], reverse=True)
print(students)
# 输出:[('小红', 95), ('小嘉', 90), ('阿明', 85)]
4.3、lambda 也能做闭包
def outer(x):
return lambda y: x + y # 直接返回一个 lambda
add5 = outer(5)
print(add5(3)) # 输出:8
print(add5(10)) # 输出:15
五、偏函数 functools.partial
functools.partial 可以"固定一个函数的部分参数",造出一个新函数。适合把已有函数改造成"少传一个参数"的专用版本:
from functools import partial
def power(base, exp):
return base ** exp
# 固定 base=2,造一个"算 2 的 n 次方"的新函数
square_of2 = partial(power, 2)
print(square_of2(10)) # 输出:1024
# 也可以固定关键字参数
int_base16 = partial(int, base=16)
print(int_base16("ff")) # 输出:255(把十六进制字符串转成十进制)
练习题
- 用递归写一个函数
digit_sum(n),返回一个整数各位数字之和(例如digit_sum(123)得 6)。 - 写一个递归函数
reverse_str(s),把字符串反转(例如"abc"→"cba")。 - 写一个闭包
make_adder(n),返回一个函数,该函数接收 x 并返回x + n;测试add3 = make_adder(3),看add3(10)是否等于 13。 - 把下面这个列表按每个字典的
"score"从高到低排序:people = [{"name": "A", "score": 80}, {"name": "B", "score": 95}],用sorted(..., key=lambda ...)。 - 思考题:为什么 Python 里尾递归写了也不能避免栈溢出?如果要处理很深的递归任务,Python 里你会改用什么?
- 用
functools.partial把print改造成一个输出时自动加分隔线的小函数(提示:固定sep、end参数)。
小结
- 递归 = 自己调自己,必须有终止条件 + 每次向终止条件逼近;太深会
RecursionError。 - 尾递归理论上可优化,但 Python 不做尾递归优化,深问题改用循环。
- 作用域按 LEGB 查找;改全局变量用
global,改外层函数变量用nonlocal。 - 闭包 = 嵌套函数 + 返回内部函数 + 记住外部变量;注意循环造闭包的"延迟绑定"陷阱,用
i=i钉住。 - lambda 是一句话匿名函数,主要配合
sorted/map/filter使用。 functools.partial用来固定已有函数的部分参数,造专用新函数。
扩展阅读
- Python 官方:嵌套函数与闭包 https://docs.python.org/3/tutorial/controlflow.html#python-scopes-and-namespaces
- PEP 227(嵌套作用域):https://peps.python.org/pep-0227/
- functools 文档:https://docs.python.org/3/library/functools.html

浙公网安备 33010602011771号