Python函数-函数高级
一、嵌套函数(nested function / inner function)
1、定义
- 在一个 def 语句的内部再写一个 def 语句,里面的函数就叫嵌套函数(inner / nested function),外面的函数叫外层函数(outer / enclosing function)。
def outer(x): y = 10 def inner(z): # ← 嵌套函数 return x + y + z return inner2、语法规则速览
- 嵌套深度不限,可以
f1里套f2,f2里再套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声明。
def outer(): cnt = 0 def inner(step=1): nonlocal cnt # 不写则 UnboundLocalError cnt += step return cnt return inner
counter = outer()print(counter(), counter()) # 1 25、闭包(closure)——嵌套函数 + 自由变量
- 满足三个条件:1. 函数嵌套;2. 内层引用外层变量;3. 外层返回内层。
- 底层结构:
>>> counter.__code__.co_freevars('cnt',)>>> counter.__closure__[0].cell_contents2二、函数闭包(function closure)
1、定义
- 闭包 = 函数对象 + 外层作用域的“现场快照”。
- 即使外层函数已经执行完毕,闭包仍然能访问那些被捕获的变量。
2、产生的三个硬条件(面试常问)
- 必须有函数嵌套;
- 内层函数引用了外层非全局变量(free variable);
- 外层函数把内层函数返回(或作为参数、塞进容器)。
def outer(x): y = 10 def inner(z): return x + y + z # 引用了 x、y return inner # 返回内层函数
f = outer(2) # 闭包诞生print(f(3)) # 2 + 10 + 3 = 153、底层原理
| 场景 | 结果 |
|---|---|
| 可变对象内容修改 | 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、查看现场
>>> f.__closure__(<cell at 0x...: int object at 0x...>, <cell at 0x...: int object at 0x...>)>>> f.__closure__[0].cell_contents2>>> f.__closure__[1].cell_contents104、常见陷阱与对策
4.1、循环变量闭包陷阱
- 原因:所有回调捕获同一个 cell,循环结束后变量定格在最终值。
funcs = [lambda: i for i in range(3)]print([f() for f in funcs]) # [2, 2, 2]- 修复:用默认参数把值“冻”在定义时
funcs = [lambda i=i: i for i in range(3)]4.2、延迟绑定(late binding)
- 类似 4.1,常见于 GUI、回调注册。
- 解决:再加一层闭包或用
functools.partial。
4.3、nonlocal 忘记写
- 在内层赋值会默认创建局部变量,导致
UnboundLocalError。
def counter(): cnt = 0 def inc(): nonlocal cnt # 必须声明 cnt += 1 return cnt return inc5、实用模式
5.1、函数工厂(配置固化)
def power(n): def f(x): return x ** n return f
square = power(2)cube = power(3)5.2、私有状态(轻量级对象)
def make_bank(initial=0): balance = initial def atm(action, amount=0): nonlocal balance if action == 'deposit': balance += amount elif action == 'withdraw': if amount <= balance: balance -= amount return amount else: raise ValueError('NSF') return balance return atm
my_acct = make_bank(100)my_acct('deposit', 50) # 150my_acct('withdraw', 30) # 1205.3、装饰器骨架(高阶 + 闭包)
import timefrom functools import wraps
def timeit(fn): @wraps(fn) def wrapper(*args, **kw): t0 = time.perf_counter() ret = fn(*args, **kw) print('cost:', time.perf_counter() - t0) return ret return wrapper三、函数装饰器(function decorator)
1、定义
- “用一个可调用对象(函数或类)去 包装(wrap) 另一个可调用对象,从而 无侵入地增强其行为”的设计模式。
2、语法糖与手工展开
@decoratordef original(x): return x + 1
# 等价于def original(x): return x + 1original = decorator(original)- 执行时机:模块导入时立即执行一次,返回的新函数被绑定到原名字。
- 装饰器本身可以是:函数、类、lambda、partial 对象,只要最终返回一个 callable。
3、无参装饰器模板(最常用)
from functools import wraps
def log(func): @wraps(func) # 保留原函数元数据 def wrapper(*args, **kw): print('call', func.__name__) ret = func(*args, **kw) print('done') return ret return wrapper关键点
- 嵌套函数形成闭包,保存原函数引用。
@wraps复制__name__/__doc__/__annotations__,否则调试信息会丢失。
4、带参装饰器(三层嵌套)
def repeat(n): def decorator(func): @wraps(func) def wrapper(*args, **kw): for _ in range(n): ret = func(*args, **kw) return ret return wrapper return decorator
@repeat(3)def hello(): print('hi')- 执行顺序:
repeat(3)→ 返回 装饰器 →@decorator再装饰hello。
4.1、先忘掉 @,只看纯函数调用
- 目标:写一个装饰器,它自己先接收参数,再去装饰真正的函数。
@repeat(3)def hello(): print('hi')# 等价裸写:def hello(): print('hi')
hello = repeat(3)(hello)- 注意最后的调用链:
repeat(3)先执行一次,返回一个新的“真正的装饰器”,再把这个装饰器应用到hello。
4.2、把三层拆开起名,便于跟踪
def repeat(n): # 第 1 层:接收装饰器参数 def decorator(func): # 第 2 层:拿到被装饰函数 @wraps(func) def wrapper(*args, **kw): # 第 3 层:真正包裹原函数 for _ in range(n): # 使用第 1 层的 n ret = func(*args, **kw) # 调用第 2 层的 func return ret return wrapper return decorator- 第 1 层:
repeat(n)只是一个普通函数,它返回下一层函数decorator。 - 第 2 层:
decorator(func)才是经典意义上的“装饰器”,它返回wrapper。 - 第 3 层:
wrapper(*args, **kw)最终替代原函数被调用。
4.3、逐帧执行流程(按时间线)
-
模块导入时
repeat(3) # 第 1 层被调用└─ 创建局部变量 n = 3└─ 返回函数对象 decorator (闭包,捕获 n) -
@repeat(3)立即把decorator应用到hellodecorator(hello) # 第 2 层被调用└─ 创建局部变量 func = hello└─ 返回函数对象 wrapper (闭包,捕获 n 和 func) -
之后每次
hello()实际执行wrapper() # 第 3 层被调用└─ 使用捕获的 n=3 循环└─ 使用捕获的 func 去执行原 hello
4.4、变量作用域示意
| 名称 | 定义位置 | 被谁引用 | 生命周期 |
|---|---|---|---|
n | repeat 局部 | decorator & wrapper 闭包 | 直到 wrapper 被回收 |
func | decorator 局部 | wrapper | 同上 |
*args, **kw | wrapper 形参 | 无 | 每次调用新建 |
4.5、如果只有两层会怎样?
- 错误示范:
def repeat(n, func): # 只有两层 @wraps(func) def wrapper(*args, **kw): for _ in range(n): ret = func(*args, **kw) return ret return wrapper- 使用:
# 必须手动传两次hello = repeat(3, hello)# 无法写成 @repeat(3)- 缺少第 1 层 ⇒ 无法先“喂”参数再装饰,因此必须拆成三层。
5、叠加装饰器(顺序从近到远)
@d2@d1def f(): ...# 等价于 f = d2(d1(f))6、类装饰器(可维护状态)
class count_calls: def __init__(self, func): self.func = func self.n = 0
def __call__(self, *args, **kw): self.n += 1 print('call #', self.n) return self.func(*args, **kw)
@count_callsdef add(a, b): return a + b- 优点:可保存状态(如计数、缓存);缺点:需要实现
__call__。
7、典型实战套路
7.1、计时器
import time, functools
def timeit(func): @functools.wraps(func) def wrapper(*args, **kw): t0 = time.perf_counter() ret = func(*args, **kw) print('cost', time.perf_counter() - t0) return ret return wrapper7.2、重试(backoff)
import time, functools
def retry(times=3, delay=1): def decorator(func): @functools.wraps(func) def wrapper(*args, **kw): for i in range(times): try: return func(*args, **kw) except Exception as e: if i == times - 1: raise e time.sleep(delay) return wrapper return decorator7.3、缓存(LRU)
@functools.lru_cache(maxsize=128)def fib(n): return n if n < 2 else fib(n-1) + fib(n-2)7.4、Flask 路由
@app.route('/hello')def hello(): return 'hello'8、常见陷阱与调试技巧
| 坑 | 描述 | 解决 |
|---|---|---|
| 元数据丢失 | wrapper.__name__ 变成 'wrapper' | 用 @functools.wraps(func) |
| 执行时机 | 装饰器在 import 时执行,可能过早 | 用工厂函数或 if __name__ == '__main__': 控制 |
| 位置参数/关键字参数维护 | 忘记 *args, **kw 导致签名不匹配 | 用 inspect.signature 检查 |
| 叠加顺序 | 先 @log 再 @cache 会每次打印 | 按需调换顺序 |