Python · 闭包、装饰器与 functools

函数是一等公民之后: 闭包捕获的是"细胞"不是值, 装饰器是洋葱不是标签, functools 让装饰后的函数保住签名、省掉重算

装饰器洋葱模型 调用: auth → log → retry → fn · 返回逆序 调用 @auth (最外层) 验 token · 不过直接 401 @log (审计) 记进入/退出 · 耗时 ms @retry (重试) 失败 → 退避再试 fn() 业务逻辑 被层层包裹 成"洋葱芯" stack 顶部 @auth 最后套上 → 最先执行 返回 @ 本质: fn = deco(fn) def 时 (import 时) 就执行并重绑定 fn 叠放从下往上包: 离 fn 最近的先包 顶层装饰器拿到的是"半成品洋葱" 闭包 = 函数 + 细胞 def make(): n = 0 · def inner(): return n return inner make() 结束后 n 不死 —— inner 背着它回家 (函数 + 环境) inner.__closure__ cell → n cell 存引用不存拷贝: n 变它跟着变 nonlocal n = 声明"改的就是这个 cell" c = make(); c() → 1, 2, 3 ... 延迟绑定 — 共享同一个 cell fs = [lambda: i for i in range(3)] 3 个 lambda 引用同一个 i 的 cell 循环结束 i=2 → fs[0]() 也是 2 ✗ 修法: lambda i=i: i (定义时绑定) 或工厂函数每次造独立 cell 闭的是"变量本身", 取值发生在调用时 lru_cache 命中路径 调用 fn(a, b) key = hash((a, b)) 参数必须可哈希 查缓存字典 hit 命中 直接返回缓存对象 零重算 ✓ miss 未命中 执行 fn(*a, **kw) 拿到返回值 cache[key] = result 移到队列头部 写入后满 (maxsize) 淘汰 LRU 尾部 三条铁律 返回值是同一个对象: 调用方原地 修改 → 缓存被污染 maxsize=None 无界增长 多进程不共享 · worker 各一份 可变默认参数的真相 — def f(x, acc=[]) 默认值在 def 时求值一次, 存进 f.__defaults__, 所有调用共享这一个 [] → 上次 append 的东西这次还在 正解: acc=None 哨兵, 函数体内 if acc is None: acc = [] Legend 机制 / 流程 装饰器层 闭包 cell 核心 / 命中 坑 / 警示

装饰器 = 可组合中间件

  • • @deco 就是 fn = deco(fn) 的糖, import 时执行
  • • 一层只管一件事: 认证/审计/重试/缓存自由叠放
  • • 调用穿心而入, 返回逆序穿出 —— 和 HTTP 中间件同构

闭包 = 函数 + 携带状态

  • • cell 存引用不存值, 变量活得比外层函数久
  • • nonlocal 声明"改 cell", 否则赋值变新建局部
  • • 延迟绑定: 闭的是变量, 取值在调用时

functools = 工程化补丁包

  • • wraps 保签名元数据, 框架反射不瞎
  • • lru_cache 守只读热路径, cache_info 进监控
  • • partial / singledispatch 是注册表与分派的武器

💡 一句话理解

函数在 Python 里是对象, 于是它可以被传来传去、包来包去、记住东西: 内层函数引用外层变量, 解释器把这对"函数 + 环境细胞"打包成闭包; 装饰器就是接收函数返回新函数的高阶函数, @auth @log @retry 像洋葱一样层层包裹业务函数 —— 调用时从外穿到里, 返回时从里穿出来。而 functools 是这套机制的"工程化补丁": wraps 保住被包裹函数的签名与文档, lru_cache 用"参数哈希 → 结果"的字典替你挡住重复计算, partial/singledispatch 把注册与分派做成了标准武器。

🧠 必知必会 必考 & 必会

LEGB 作用域
名字查找顺序 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 没被回收
延迟绑定
闭包捕获的是变量本身, 取值发生在调用时 —— 循环里建闭包全绑到最后一个 i 的根源。
fs = [lambda: i for i in range(3)]
[f() for f in fs]        # → [2, 2, 2] 全是最后一个 i
# 关键: 捕获的是变量 i 本身, 调用时才取值
nonlocal
在内层声明 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 时)执行并重绑定
带参装饰器
三层嵌套: 工厂(收参数) → deco(收函数) → wrapper(收调用参数)。@retry(3) 先调用 retry(3) 拿到真装饰器。
def retry(times):             # 工厂: 收参数
    def deco(fn):             # 装饰器: 收函数
        def wrapper(*a):      # wrapper: 收调用参数
            ...
        return wrapper
    return deco
@retry(3)                     # 关键: 先 retry(3) 拿到真装饰器
functools.wraps
把 fn 的 __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')
lru_cache / cache
key = 全参数哈希; 线程安全(内部有锁); 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 ... 可观测
cached_property
首次访问算一次并写入实例 __dict__, 之后直接读字典绕过描述符; 实例常驻就要考虑缓存失效(手动 del)。
class Report:
    @cached_property
    def rows(self):           # 首次访问才计算
        return heavy_join()
r = Report()
r.rows is r.rows              # → True: 之后读 __dict__
del r.rows                    # 主动失效, 再访问重算
partial 偏函数
预绑定部分位置/关键字参数生成新 callable; 是对象不是函数 —— 没有 __name__, 需要反射的框架要留意。
int2 = partial(int, base=2)  # 预绑定关键字参数
int2("1010")                  # → 10
p = partial(send, "email")    # 绑定首位参数
p("welcome", {})              # → send("email", "welcome", {})
hasattr(p, "__name__")        # → False: 是对象非函数
singledispatch
按第一个参数的类型注解注册分派分支 + 默认兜底, 用 OCP 替代 if isinstance 长链; 注册用 ABC 更稳。
@singledispatch
def size(x): return 1          # 默认兜底分支
@size.register
def _(x: str): return len(x)   # 按第一个参数注解分派
size("abc")                    # → 3
size(3.14)                     # → 1 走默认分支
reduce 一句话
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 是单位元素
可变默认参数真相
默认值在 def 时求值一次, 存在 __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 = []

🏭 生产实战 real world

场景 1 · 三层装饰器栈: 审计 + 重试守支付回调

支付回调入口需要"耗时审计 + 瞬时错误重试"两个横切能力, 不想侵入业务代码, 用装饰器栈组装:

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 层消化重试, 审计层只看到最终耗时 —— 每层职责单一, 叠放顺序即语义。

场景 2 · lru_cache 扛住商品元数据读放大

列表页一次请求要读几百个 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 必须接监控

场景 3 · wraps 救活 FastAPI 的参数反射

团队统一的 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 读不到注解, 
# 依赖注入/文档/序列化全瞎 —— 一个装饰器行引发的线上事故

场景 4 · partial 绑定重复参数做回调注册表

事件分发要按 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 永不改

场景 5 · 闭包实现进程内限流器 (nonlocal 正确用法)

单实例 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, 这正是"工厂函数"的价值。

场景 6 · 延迟绑定事故: 三个告警回调全重启了 mq

巡检脚本给 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 ✓

场景 7 · singledispatch 按 payload 版本分派 handler

同一 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 长链

场景 8 · cached_property 惰性求值昂贵报表

报表对象的 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 一次。

场景 9 · 带参数的重试装饰器工厂 (三层嵌套)

重试次数和退避基数要按接口配置, 装饰器必须能收参数 —— 这就是"工厂 → 装饰器 → 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(): ...

场景 10 · 装饰器做依赖注入: 把 session 织进函数参数

业务函数都想要一个 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)     # 挂进外层事务 —— 测试替身免费

⚠️ 编码注意与常见坑 pitfalls

坑 1 · 可变默认参数被调用间共享 — 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 = []
坑 2 · 装饰器忘 wraps — 装饰后 __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)
坑 3 · 循环里建闭包全绑最后一个 i — [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]
坑 4 · lru_cache 参数不可哈希 — 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))        #     或抽成可哈希元组
坑 5 · 缓存返回可变对象被原地改 — @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
坑 6 · maxsize=None 无界增长 — @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): ...
坑 7 · 被缓存函数有副作用 — 给 send_email() 挂了 lru_cache, "重发验证码"永远命中缓存不再真正发送, 重试逻辑同死。正解: 只装饰纯读函数; 写操作类函数禁止进缓存。
@lru_cache                     # 错: 第二次"重发"命中缓存,
def send_code(mobile): ...     #     不再真正发送
def send_code(mobile): ...     # 对: 写操作不进缓存
@lru_cache
def sms_tpl(id): ...            #     只装饰纯读函数
坑 8 · 多进程缓存不共享 — gunicorn 4 个 worker 各持一份 lru_cache, 命中率打折且 cache_clear() 只清当前进程。正解: 跨 worker 一致性靠 Redis/Redisson 类外部层; 进程内缓存只服务不可变数据。
# gunicorn -w 4: 每个 worker 各持一份 lru_cache
sku_meta.cache_clear()          # 错: 只清当前进程,
                                #     其他 3 个还在用旧数据
rds.delete("sku:1")           # 对: 跨 worker 一致走 Redis
#     下次 miss 时各进程重新加载
坑 9 · 装饰器执行时机误解 — 装饰逻辑写在 deco 层(而非 wrapper 层), 模块 import 时就跑了: 配置还没加载、DB 还没连。正解: deco 层只做包装, 副作用全部放 wrapper; 用 import 副作用测试守住。
def deco(fn):
    print("import 时就执行")   # 错: 模块加载即跑,
    return fn                  #     配置/DB 还没就绪
def deco(fn):
    def wrapper(*a, **kw):
        log("调用时才执行")      # 对: 副作用放 wrapper
        return fn(*a, **kw)
坑 10 · 带参装饰器少套一层 — 写了三层却按 @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(): ...
坑 11 · nonlocal 未声明 — 内层函数 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
坑 12 · 闭包拖着大对象不放 — 闭包引用了一个 800MB DataFrame, 只要 handler 活着进程内存就下不来, 常驻服务内存只涨不跌。正解: 大对象处理完 del/置 None 再长期持有闭包; 模块级注册表别绑大数据。
big = load_800mb()                  # 800MB DataFrame
def handler(): return query(big)    # 错: 闭包拖着 big 不放
register(handler)                   #     常驻服务内存只涨不跌
res = query(load_800mb())           # 对: 算完即弃
def handler(): return res           #     闭包只带小结果
坑 13 · partial 没有 __name__ — 把 partial 注册进需要反射 __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")
坑 14 · singledispatch 注册类型不精确 — 只 register 了 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, 别让它静默
坑 15 · 装饰器吞返回值 — wrapper 忘写 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
坑 16 · 装饰器吞异常改语义 — wrapper 里 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  # 对: 窄类型+重抛
坑 17 · staticmethod 叠装饰器顺序错 — 把 @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(): ...
坑 18 · reduce 空序列没初始值 — 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
坑 19 · property 与 cached_property 混用 — 父类 @cached_property 的属性在子类被 @property 覆盖, 或实例 __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                 # 对: 清掉实例字典再访问 → 新逻辑
坑 20 · lru_cache 装在实例方法上泄漏实例 — @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)