Day17 · 函数(二)递归、作用域、闭包与 lambda

Day17 · 函数(二)递归、作用域、闭包与 lambda

阶段:阶段一 · 核心基础 | 前置:Day16 | 技术基准:Python 3.15.0

学习目标

  • 理解递归的两个必要条件,会写阶乘、斐波那契等递归函数,知道递归深度限制
  • 理解尾递归概念,以及"Python 不做尾递归优化"这个重要事实
  • 掌握变量作用域:LEGB 查找规则、global、nonlocal、嵌套作用域
  • 理解闭包(Closure),会写计数器闭包,警惕闭包的"延迟绑定"陷阱
  • 会用 lambda 匿名函数配合 sorted/map/filter,了解 functools.partial 偏函数

一、递归(Recursion)

1.1、什么是递归

递归就是"函数自己调用自己"。它把一个大问题层层拆成规模更小的同类问题,直到碰到一个"不用再拆、直接能给出答案"的出口(终止条件)。写递归必须满足两点:

  1. 明确的终止条件(base case)——否则会无限递归直到崩溃。
  2. 每次调用都在向终止条件逼近(参数在变小/变化)。

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(把十六进制字符串转成十进制)

练习题

  1. 用递归写一个函数 digit_sum(n),返回一个整数各位数字之和(例如 digit_sum(123) 得 6)。
  2. 写一个递归函数 reverse_str(s),把字符串反转(例如 "abc" → "cba")。
  3. 写一个闭包 make_adder(n),返回一个函数,该函数接收 x 并返回 x + n;测试 add3 = make_adder(3),看 add3(10) 是否等于 13。
  4. 把下面这个列表按每个字典的 "score" 从高到低排序:people = [{"name": "A", "score": 80}, {"name": "B", "score": 95}],用 sorted(..., key=lambda ...)。
  5. 思考题:为什么 Python 里尾递归写了也不能避免栈溢出?如果要处理很深的递归任务,Python 里你会改用什么?
  6. 用 functools.partial 把 print 改造成一个输出时自动加分隔线的小函数(提示:固定 sep、end 参数)。

小结

  • 递归 = 自己调自己,必须有终止条件 + 每次向终止条件逼近;太深会 RecursionError。
  • 尾递归理论上可优化,但 Python 不做尾递归优化,深问题改用循环。
  • 作用域按 LEGB 查找;改全局变量用 global,改外层函数变量用 nonlocal。
  • 闭包 = 嵌套函数 + 返回内部函数 + 记住外部变量;注意循环造闭包的"延迟绑定"陷阱,用 i=i 钉住。
  • lambda 是一句话匿名函数,主要配合 sorted/map/filter 使用。
  • functools.partial 用来固定已有函数的部分参数,造专用新函数。

扩展阅读

posted @ 2026-11-09 20:32  小嘉游戏  阅读(8)  评论(0)    收藏  举报