Python函数-函数高级
2025-8-31
| 2025-8-31
Words 2343Read Time 6 min
type
status
date
slug
summary
tags
category
icon
password

📝 嵌套函数(nested function / inner function)

1、定义

在一个 def 语句的内部再写一个 def 语句,里面的函数就叫嵌套函数(inner / nested function),外面的函数叫外层函数(outer / enclosing function)。

2、语法规则速览

嵌套深度不限,可以 f1 里套f2f2里再套 f3
内层函数名仅在外层函数局部作用域可见;外层无法直接调用 inner()。
内层函数可以被返回、塞进容器、当作参数传递,从而在外层函数返回后仍然存活——这就是闭包

3、生命周期(LEGB 视角)

3.1、编译阶段

Python 看到 def inner 时,会为 inner 生成一个代码对象 inner.__code__,并记录:
  • co_freevars:在内层用到、却定义在外层的自由变量名(free variables)。
  • co_cellvars:外层定义、被内层引用的被捕获变量名。

3.2、运行阶段

外层函数被调用 → 创建局部变量 → 遇到 return inner → 返回一个函数对象,其 __closure__ 属性指向若干 cell 对象,每个 cell 保存一个自由变量的当前值。

3.3、外层函数返回后

栈帧销毁,但 cell 仍被内层函数引用 → 变量继续存活 → 闭包诞生

4、作用域与名字查找(LEGB)

Local → Enclosing → Global → Builtin
  • 外层变量:直接可读。
  • 外层变量:默认会创建同名局部变量,除非显式声明 nonlocal
  • 全局变量:需 global 声明。

5、闭包(closure)——嵌套函数 + 自由变量

满足三个条件:1. 函数嵌套;2. 内层引用外层变量;3. 外层返回内层。
底层结构:

📝 函数闭包(function closure)

1、定义

闭包 = 函数对象 + 外层作用域的“现场快照”
即使外层函数已经执行完毕,闭包仍然能访问那些被捕获的变量。

2、产生的三个硬条件(面试常问)

  1. 必须有函数嵌套
  1. 内层函数引用了外层非全局变量(free variable);
  1. 外层函数把内层函数返回(或作为参数、塞进容器)。

3、底层原理

场景
结果
可变对象内容修改
cell 仍指向同一对象,内容可变
重新赋值(无 nonlocal
创建新的局部变量,不再走 cell
循环变量陷阱
所有 lambda 共享同一 cell,需用默认参数冻结

3.1、编译阶段

inner.__code__.co_freevars('x', 'y') 声明需要外层的哪些变量。
outer.__code__.co_cellvars('x', 'y') 声明要提供给内层做闭包。

3.2、运行阶段

每产生一个自由变量,就创建一个 cell 对象,位于 frame.f_localsplus
外层返回后,栈帧销毁,但 cell 仍被 inner.__closure__ 引用,值继续存活。

3.3、查看现场

4、常见陷阱与对策

4.1、循环变量闭包陷阱

原因:所有回调捕获同一个 cell,循环结束后变量定格在最终值。
修复:用默认参数把值“冻”在定义时

4.2、延迟绑定(late binding)

类似 4.1,常见于 GUI、回调注册。
解决:再加一层闭包或用 functools.partial

4.3、nonlocal 忘记写

在内层赋值会默认创建局部变量,导致 UnboundLocalError

5、实用模式

5.1、函数工厂(配置固化)

5.2、私有状态(轻量级对象)

5.3、装饰器骨架(高阶 + 闭包)

📝 函数装饰器(function decorator)

1、定义

“用一个可调用对象(函数或类)去 包装(wrap) 另一个可调用对象,从而 无侵入地增强其行为”的设计模式。

2、语法糖与手工展开

执行时机:模块导入时立即执行一次,返回的新函数被绑定到原名字。
装饰器本身可以是:函数、类、lambda、partial 对象,只要最终返回一个 callable。

3、无参装饰器模板(最常用)

关键点

嵌套函数形成闭包,保存原函数引用。
@wraps 复制 __name__/__doc__/__annotations__,否则调试信息会丢失。

4、带参装饰器(三层嵌套)

执行顺序:repeat(3) → 返回 装饰器@decorator 再装饰 hello

4.1、先忘掉 @,只看纯函数调用

目标:写一个装饰器,它自己先接收参数,再去装饰真正的函数。
注意最后的调用链:repeat(3) 先执行一次,返回一个新的“真正的装饰器”,再把这个装饰器应用到 hello

4.2、把三层拆开起名,便于跟踪

第 1 层repeat(n)只是一个普通函数,它返回下一层函数 decorator
第 2 层decorator(func) 才是经典意义上的“装饰器”,它返回 wrapper
第 3 层wrapper(*args, **kw) 最终替代原函数被调用。

4.3、逐帧执行流程(按时间线)

  1. 模块导入时
    1. @repeat(3) 立即把 decorator 应用到 hello
      1. 之后每次 hello() 实际执行

        4.4、变量作用域示意

        名称
        定义位置
        被谁引用
        生命周期
        n
        repeat 局部
        decorator & wrapper 闭包
        直到 wrapper 被回收
        func
        decorator 局部
        wrapper
        同上
        *args, **kw
        wrapper 形参
        每次调用新建

        4.5、如果只有两层会怎样?

        • 错误示范:
          • 使用:
            缺少第 1 层 ⇒ 无法先“喂”参数再装饰,因此必须拆成三层。

            5、叠加装饰器(顺序从近到远)

            6、类装饰器(可维护状态)

            优点:可保存状态(如计数、缓存);缺点:需要实现 __call__

            7、典型实战套路

            7.1、计时器

            7.2、重试(backoff)

            7.3、缓存(LRU)

            7.4、Flask 路由

            8、常见陷阱与调试技巧

            描述
            解决
            元数据丢失
            wrapper.__name__ 变成 'wrapper'
            @functools.wraps(func)
            执行时机
            装饰器在 import 时执行,可能过早
            用工厂函数或 if __name__ == '__main__': 控制
            位置参数/关键字参数维护
            忘记 *args, **kw 导致签名不匹配
            inspect.signature 检查
            叠加顺序
            @log@cache 会每次打印
            按需调换顺序
          • 开发
          • Python
          • Python函数-函数进阶Python函数-匿名函数、生成器、内置函数和推导式
            Loading...