skip to content
Logo Lostman_Wang的小站

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 inner

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 声明。
def outer():
cnt = 0
def inner(step=1):
nonlocal cnt # 不写则 UnboundLocalError
cnt += step
return cnt
return inner
counter = outer()
print(counter(), counter()) # 1 2

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

  • 满足三个条件:1. 函数嵌套;2. 内层引用外层变量;3. 外层返回内层。
  • 底层结构:
>>> counter.__code__.co_freevars
('cnt',)
>>> counter.__closure__[0].cell_contents
2

二、函数闭包(function closure)

1、定义

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

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

  1. 必须有函数嵌套
  2. 内层函数引用了外层非全局变量(free variable);
  3. 外层函数把内层函数返回(或作为参数、塞进容器)。
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 = 15

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、查看现场

>>> f.__closure__
(<cell at 0x...: int object at 0x...>,
<cell at 0x...: int object at 0x...>)
>>> f.__closure__[0].cell_contents
2
>>> f.__closure__[1].cell_contents
10

4、常见陷阱与对策

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 inc

5、实用模式

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) # 150
my_acct('withdraw', 30) # 120

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

import time
from 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、语法糖与手工展开

@decorator
def original(x):
return x + 1
# 等价于
def original(x):
return x + 1
original = 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、逐帧执行流程(按时间线)

  1. 模块导入时

    repeat(3) # 第 1 层被调用
    └─ 创建局部变量 n = 3
    └─ 返回函数对象 decorator (闭包,捕获 n)
  2. @repeat(3) 立即把 decorator 应用到 hello

    decorator(hello) # 第 2 层被调用
    └─ 创建局部变量 func = hello
    └─ 返回函数对象 wrapper (闭包,捕获 n 和 func)
  3. 之后每次 hello() 实际执行

    wrapper() # 第 3 层被调用
    └─ 使用捕获的 n=3 循环
    └─ 使用捕获的 func 去执行原 hello

4.4、变量作用域示意

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

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
@d1
def 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_calls
def 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 wrapper

7.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 decorator

7.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 会每次打印按需调换顺序