函数是一等公民之后: 闭包捕获的是"细胞"不是值, 装饰器是洋葱不是标签, functools 让装饰后的函数保住签名、省掉重算
函数在 Python 里是对象, 于是它可以被传来传去、包来包去、记住东西: 内层函数引用外层变量, 解释器把这对"函数 + 环境细胞"打包成闭包; 装饰器就是接收函数返回新函数的高阶函数, @auth @log @retry 像洋葱一样层层包裹业务函数 —— 调用时从外穿到里, 返回时从里穿出来。而 functools 是这套机制的"工程化补丁": wraps 保住被包裹函数的签名与文档, lru_cache 用"参数哈希 → 结果"的字典替你挡住重复计算, partial/singledispatch 把注册与分派做成了标准武器。
Local → Enclosing → Global → Builtin, 找到即停。闭包就是其中的 E 层: 内层函数看得见外层函数的局部名。 x = "global" # G def outer(): x = "enclosing" # E — 闭包看的就是这层 def inner(): x = "local" # L — 找到即停 return x return inner() # → "local"; 删掉 inner 里的赋值 → "enclosing"
__closure__(cell 元组), 每个细胞包着对外层变量的引用而非拷贝; 只要闭包活着, 这些变量就不回收。 def make(): n = 0 # n 活在 cell 里 def inner(): return n # 持引用, 不是拷贝 return inner f = make() f.__closure__ # → 含 1 个 cell, n 没被回收
fs = [lambda: i for i in range(3)] [f() for f in fs] # → [2, 2, 2] 全是最后一个 i # 关键: 捕获的是变量 i 本身, 调用时才取值
nonlocal n 表示"赋值目标是外层那个 cell"; 不声明就赋值会被当成新建局部名, 抛 UnboundLocalError。 def counter(): n = 0 def inc(): nonlocal n # 关键: 改的是外层 cell n += 1 return n return inc c = counter(); c(); c() # → 1, 2
@deco def fn() 等价于 fn = deco(fn), 在 def 时(import 时)执行; 叠放自下而上, 离函数近的先包。 def deco(fn): def wrapper(*a, **kw): return fn(*a, **kw) return wrapper @deco # 等价于 fn = deco(fn) def fn(): ... # 关键: def 时(import 时)执行并重绑定
@retry(3) 先调用 retry(3) 拿到真装饰器。 def retry(times): # 工厂: 收参数 def deco(fn): # 装饰器: 收函数 def wrapper(*a): # wrapper: 收调用参数 ... return wrapper return deco @retry(3) # 关键: 先 retry(3) 拿到真装饰器
__name__/__doc__/__module__/__wrapped__ 复制给 wrapper, 让 inspect/FastAPI/文档/序列化反射正常工作 —— wrapper 的标配。 def traced(fn): @functools.wraps(fn) # 复制签名/文档/__wrapped__ def wrapper(*a, **kw): return fn(*a, **kw) return wrapper @traced def pay(oid): ... pay.__name__ # → 'pay' (无 wraps 则 'wrapper')
cache_info()/cache_clear() 可观测; cache 等价 maxsize=None(慎用)。 @lru_cache(maxsize=128) def fib(n): return n if n < 2 else fib(n-1) + fib(n-2) fib(30); fib(30) # 第二次零重算 fib.cache_info() # → hits=1 ... 可观测
__dict__, 之后直接读字典绕过描述符; 实例常驻就要考虑缓存失效(手动 del)。 class Report: @cached_property def rows(self): # 首次访问才计算 return heavy_join() r = Report() r.rows is r.rows # → True: 之后读 __dict__ del r.rows # 主动失效, 再访问重算
__name__, 需要反射的框架要留意。 int2 = partial(int, base=2) # 预绑定关键字参数 int2("1010") # → 10 p = partial(send, "email") # 绑定首位参数 p("welcome", {}) # → send("email", "welcome", {}) hasattr(p, "__name__") # → False: 是对象非函数
@singledispatch def size(x): return 1 # 默认兜底分支 @size.register def _(x: str): return len(x) # 按第一个参数注解分派 size("abc") # → 3 size(3.14) # → 1 走默认分支
reduce(f, seq, init) 左折叠; init 千万别省(空序列抛 TypeError); 大多数场景 sum/max/zip 更清晰。 reduce(lambda a, b: a + b, [1, 2, 3, 4]) # → ((1+2)+3)+4 = 10 — 左折叠 reduce(add, []) # → TypeError: 空 + 无 init reduce(add, [], 0) # → 0: init 是单位元素
__defaults__, 调用间共享同一个 []/{} —— 想每次新对象用 None 哨兵。 def f(x, acc=[]): # 默认值 def 时求值一次 acc.append(x); return acc f(1) # → [1] f(2) # → [1, 2] 上次还在! def g(x, acc=None): # 对: None 哨兵 if acc is None: acc = []
支付回调入口需要"耗时审计 + 瞬时错误重试"两个横切能力, 不想侵入业务代码, 用装饰器栈组装:
import functools, time, logging def audit(fn): # 最外层: 记进入/退出与耗时 @functools.wraps(fn) def wrapper(*a, **kw): t0 = time.monotonic() try: return fn(*a, **kw) # 返回值必须透传, 不能吞 finally: logging.info("%s %.1fms", fn.__name__, (time.monotonic() - t0) * 1e3) return wrapper @audit # 自下而上应用: retry 先包住业务 @retry(times=3) # retry 工厂见场景 9 def call_payment(oid): return gateway.charge(oid)
调用链: audit → retry → charge; charge 抛 TransientError 时在 retry 层消化重试, 审计层只看到最终耗时 —— 每层职责单一, 叠放顺序即语义。
列表页一次请求要读几百个 SKU 元数据, 重复 SKU 与跨请求重复打到 DB, 读 QPS 3200; 元数据是只读热数据, 正是 lru_cache 的主场:
from functools import lru_cache @lru_cache(maxsize=4096) # 有上界: 防无界增长 def sku_meta(sku_id: int) -> dict: return db.one("SELECT price,title FROM sku WHERE id=%s", (sku_id,)) def render_page(skus): return [sku_meta(s) for s in skus] # 重复 SKU 直接命中 # 上线后: 元数据 DB QPS 3200 → 约 100 (命中率 97%), 列表页 P99 210ms → 18ms assert sku_meta.cache_info().hits > 0 # cache_info 必须接监控
团队统一的 traced 装饰器上线后, 所有路由的 OpenAPI schema 塌成 (*args, **kwargs), 前端批量 422; 缺的就是 @wraps:
import functools, inspect def traced(fn): @functools.wraps(fn) # 复制 __name__/__doc__/__wrapped__/签名 def wrapper(*args, **kw): return fn(*args, **kw) return wrapper @traced def create_user(name: str, age: int = 18): ... inspect.signature(create_user) # → (name: str, age: int = 18) ✓ # 没有 wraps 时 __name__=='wrapper', FastAPI 读不到注解, # 依赖注入/文档/序列化全瞎 —— 一个装饰器行引发的线上事故
事件分发要按 topic 路由到"频道 + 事件名"组合, 不想写一堆胶水函数, 用 partial 预绑定前两个参数:
from functools import partial def send(channel: str, event: str, payload: dict): ... ROUTES = { # 注册表: 预绑定固定的两个参数 "order.paid": partial(send, "email", "order_paid"), "order.refunded": partial(send, "sms", "order_refund"), "user.signup": partial(send, "push", "welcome"), } def dispatch(topic: str, payload: dict): ROUTES[topic](payload) # 新事件 = 注册表加一行, dispatch 永不改
单实例 QPS 限流不想引 Redis, 用闭包把窗口状态藏在函数里, 零类零全局变量:
import time def rate_limiter(max_qps: int): window, count = 0.0, 0 # 状态活在 cell 里 def allow() -> bool: nonlocal count, window # 声明: 改的是外层 cell now = time.monotonic() if now - window >= 1.0: window, count = now, 0 # 滚进新的一秒窗口 count += 1 return count <= max_qps return allow allow = rate_limiter(100) # 每个 limiter 独立一套 cell
两个 limiter 互不干扰 —— make 每调用一次就造一组新 cell, 这正是"工厂函数"的价值。
巡检脚本给 db/cache/mq 三个组件各注册一个重启回调, 告警触发时三个回调全重启了最后一个组件 mq:
services = ["db", "cache", "mq"] # 事故代码: 三个 lambda 共享同一个 svc 的 cell, 取值在调用时 # handlers = [lambda: restart(svc) for svc in services] # handlers[0]() → restart("mq") —— 三个回调全重启了最后一个 ✗ # 修法一: 默认参数在 def 时求值, 把当前值钉住 handlers = [lambda svc=svc: restart(svc) for svc in services] # 修法二: 工厂函数, 每次调用造独立 cell def bind(svc): def _restart(): return restart(svc) return _restart handlers = [bind(s) for s in services] # handlers[0]() 正确重启 db ✓
同一 topic 的消息体有 V1/V2 两代结构, if isinstance 分支越积越多; 换成类型分派, 新版本=新注册:
from functools import singledispatch @singledispatch def render(payload) -> dict: # 默认兜底分支必须有 return {"type": "unknown"} @render.register def _(p: V2Payload) -> dict: # 按第一个参数的注解分派 return {"v": 2, **p.model_dump()} @render.register def _(p: V1Payload) -> dict: return {"v": 1, **migrate_v1(p)} # 老版本走兼容分支 def on_request(req): return render(parse_body(req)) # 干掉 if isinstance 长链
报表对象的 rows 要跑一次 1.8s 的多表 join, 但九成请求只用到摘要字段, 把 rows 做成惰性缓存:
from functools import cached_property class MemberReport: def __init__(self, uid: int): self.uid = uid @cached_property def rows(self) -> list: # 首次访问才执行, 之后读实例 __dict__ return heavy_join(self.uid) r = MemberReport(42) r.rows is r.rows # True: 第二次不再进描述符 # 注意: 实例常驻 (如 session 级缓存) 时数据会陈旧, # 主动失效: del r.rows 之后又会走一次真正的计算
摘要类请求 P99 从 1.9s 回到 30ms; 全量请求同实例只 join 一次。
重试次数和退避基数要按接口配置, 装饰器必须能收参数 —— 这就是"工厂 → 装饰器 → wrapper"三层嵌套:
import functools, time def retry(times=3, backoff=0.2): # 三层: 工厂(参数) → deco(函数) → wrapper def deco(fn): @functools.wraps(fn) def wrapper(*a, **kw): last = None for i in range(times): try: return fn(*a, **kw) except TransientError as e: # 捕窄: 业务错误不重试 last = e; time.sleep(backoff * 2 ** i) # 指数退避 raise last return wrapper return deco @retry(times=5, backoff=0.1) # 先调用 retry(5, 0.1) 拿到真装饰器 def sync_inventory(): ...
业务函数都想要一个 DB session, 但手动管理 open/commit/rollback 到处重复; 用装饰器按需注入, 外层事务还能透传:
import functools def with_session(fn): @functools.wraps(fn) def wrapper(*args, session=None, **kw): if session is not None: # 调用方传了: 加入外层事务 return fn(*args, session=session, **kw) with Session() as s: # 没传: 这里管生命周期 return fn(*args, session=s, **kw) return wrapper @with_session def transfer(src, dst, amount, session): ... transfer(1, 2, 99.0) # 自动开/提交/回滚 transfer(1, 2, 99.0, session=outer_tx) # 挂进外层事务 —— 测试替身免费
def f(x, acc=[]) 里 acc.append(x), 下次调用上次的结果还在, 越积越多。原因: 默认值在 def 时求值一次存进 __defaults__, 所有调用共享。正解: acc=None 哨兵 + 函数体内 if acc is None: acc = []。 def f(x, acc=[]): acc.append(x); return acc f(1) # 错: → [1] f(2) # → [1, 2] 两次共享同一个 [] def f(x, acc=None): # 对: None 哨兵, 每次新 [] if acc is None: acc = []
__name__ 变 'wrapper', 文档、inspect.signature、FastAPI 依赖注入、序列化器全瞎。正解: wrapper 上一律加 @functools.wraps(fn), code review 当硬性检查项。 def deco(fn): def wrapper(*a, **kw): return fn(*a, **kw) return wrapper # 错: __name__ → 'wrapper', @deco # inspect/FastAPI 反射全瞎 def pay(oid): ... def deco(fn): @functools.wraps(fn) # 对: 复制签名/文档元数据 def wrapper(*a, **kw): return fn(*a, **kw)
[lambda: i for i in range(3)] 三个函数都返回 2。原因: 延迟绑定, 所有 lambda 共享同一个 i 的 cell。正解: lambda i=i: i 或工厂函数每次造独立 cell。 fs = [lambda: i for i in range(3)] [f() for f in fs] # 错: → [2, 2, 2] 共享同一 cell fs = [lambda i=i: i for i in range(3)] # 对: 定义时钉住值 [f() for f in fs] # → [0, 1, 2]
get_meta({"id": 1}) 直接 TypeError: unhashable type: 'dict'。原因: 缓存 key 要进字典。正解: 传 int/str 等可哈希标识; 结构化参数先抽成可哈希元组。 @lru_cache def get_meta(f): ... get_meta({"id": 1}) # 错: TypeError: unhashable type: 'dict' get_meta(1) # 对: 传可哈希标识 get_meta(("v2", 1)) # 或抽成可哈希元组
@lru_cache def load(): return [], 调用方 append 之后所有后续命中都拿到脏数据。正解: 缓存函数内返回 tuple/深拷贝, 或约定只读; 文档里写明"返回值禁止修改"。 @lru_cache def load(): return {"rows": []} d = load(); d["rows"].append(x) # 错: 原地改, # 之后所有命中都拿到脏数据 def load(): return (r1, r2) # 对: 不可变 tuple
@cache 缓存了含 user_id/请求参数的函数, key 随调用方无限增长, 长跑进程内存泄漏。正解: 给 maxsize 设上界; 高基数场景改 Redis 外部缓存配 TTL。 @cache # 错: 等价 maxsize=None, 无界 def perms(user_id, req_id): ... # key 随调用无限增长 @lru_cache(maxsize=8192) # 对: 设上界; 高基数改 Redis+TTL def perms(user_id): ...
send_email() 挂了 lru_cache, "重发验证码"永远命中缓存不再真正发送, 重试逻辑同死。正解: 只装饰纯读函数; 写操作类函数禁止进缓存。 @lru_cache # 错: 第二次"重发"命中缓存, def send_code(mobile): ... # 不再真正发送 def send_code(mobile): ... # 对: 写操作不进缓存 @lru_cache def sms_tpl(id): ... # 只装饰纯读函数
cache_clear() 只清当前进程。正解: 跨 worker 一致性靠 Redis/Redisson 类外部层; 进程内缓存只服务不可变数据。 # gunicorn -w 4: 每个 worker 各持一份 lru_cache sku_meta.cache_clear() # 错: 只清当前进程, # 其他 3 个还在用旧数据 rds.delete("sku:1") # 对: 跨 worker 一致走 Redis # 下次 miss 时各进程重新加载
def deco(fn): print("import 时就执行") # 错: 模块加载即跑, return fn # 配置/DB 还没就绪 def deco(fn): def wrapper(*a, **kw): log("调用时才执行") # 对: 副作用放 wrapper return fn(*a, **kw)
@retry(不带括号)用, fn 被当成 times 传进去, 调用时 'NoneType' object is not callable。正解: 保持 @retry() 用法一致, 或在工厂里判断第一个参数是不是 callable 做双模式兼容。 @retry # 错: fn 被当成 times 传进去, def call(): ... # 调用 → 'NoneType' object is not callable @retry(times=3) # 对: 先调用工厂拿真装饰器 def call(): ...
count += 1 抛 UnboundLocalError。原因: 赋值让解释器把 count 当新建局部名, 用在赋值前。正解: 显式 nonlocal count, 或状态放 list/dict 里原地改。 def counter(): n = 0 def inc(): n += 1 # 错: UnboundLocalError return n def counter(): n = 0 def inc(): nonlocal n # 对: 声明改的是外层 cell n += 1; return n
del/置 None 再长期持有闭包; 模块级注册表别绑大数据。 big = load_800mb() # 800MB DataFrame def handler(): return query(big) # 错: 闭包拖着 big 不放 register(handler) # 常驻服务内存只涨不跌 res = query(load_800mb()) # 对: 算完即弃 def handler(): return res # 闭包只带小结果
__name__ 的框架(Celery 任务/CLI/注册表), 报错或名字全叫 'partial'。正解: 外面再包一层 def, 或用 functools.wraps(原函数) 包装 partial 对象。 task = partial(export, fmt="csv") celery.task(task) # 错: 报错或名字全叫 'partial' @celery.task # 对: 外面再包一层 def def export_csv(): return export(fmt="csv")
dict, 来了 Mapping/OrderedDict 或 numpy str 走默认分支, 静默输出 unknown。正解: 注册 collections.abc 抽象类型; 默认分支里加显式告警日志, 别让它静默。 @render.register def _(p: dict): ... # 错: Mapping/OrderedDict render(OrderedDict()) # 静默走默认分支 → unknown @render.register # 对: 注册 abc 抽象类型 def _(p: Mapping): ... # 默认分支里 log.warning, 别让它静默
return func(...), 被装饰函数一律返回 None, 调用方拿 None 取下标才炸。正解: wrapper 模板固定 return func(*args, **kwargs), 有后处理也要 return 结果。 def wrapper(*a, **kw): fn(*a, **kw) # 错: 忘 return → 一律 None def wrapper(*a, **kw): return fn(*a, **kw) # 对: 结果必须透传 # 有后处理: r = fn(...); ...; return r
except Exception: pass, 核心业务错误被压住, 上层以为成功, 数据悄悄丢。正解: 只捕自己要处理的窄类型并 raise 重抛, 保持原栈; 其余异常原样穿透。 def wrapper(*a, **kw): try: return fn(*a, **kw) except Exception: pass # 错: 业务错误被压住 def wrapper(*a, **kw): try: return fn(*a, **kw) except Retryable: log(...); raise # 对: 窄类型+重抛
@staticmethod 放在自己的装饰器里面, 类属性拿到的是未绑定的 staticmethod 对象, 调用报 'staticmethod' object is not callable。正解: @staticmethod/@classmethod 永远放最外(最上)层。 class C: @my_deco # 错: 类属性拿到 staticmethod 对象 @staticmethod # → 'staticmethod' object def f(): ... # is not callable class C: @staticmethod # 对: 永远放最外(最上)层 @my_deco def f(): ...
reduce(f, []) 抛 TypeError: reduce() of empty iterable with no initial value, 空批次/空流场景必踩。正解: 永远传第三参 reduce(f, seq, init), init 用单位元素。 reduce(add, []) # 错: TypeError: reduce() of empty # iterable with no initial value reduce(add, [], 0) # 对: 加法单位元素 → 0 reduce(mul, [], 1) # 乘法单位是 1
__dict__ 已有旧值时新逻辑"不生效", 缓存永远陈旧。正解: 同名属性只选一种策略; 失效用 del obj.attr 清掉实例字典再访问。 class A: @cached_property def data(self): return load_v1() class B(A): @property # 错: 旧 __dict__ 值还在, def data(self): ... # 新逻辑"不生效" del b.data # 对: 清掉实例字典再访问 → 新逻辑
@lru_cache def load(self, id), self 进了缓存 key, 实例被缓存钉住无法回收, 用户量一大对象越挂越多。正解: 缓存模块级纯函数 load(user_id, id), 方法里调用它, self 不进 key。 class Svc: @lru_cache # 错: self 进 key, def load(self, id): ... # 实例被缓存钉住无法回收 @lru_cache # 对: 模块级纯函数, self 不进 key def load(svc_name, id): ... # 方法里调用: return load(self.name, id)