6 · 闭包与装饰器:函数的终极形态
🔗 知识图谱导航:阅读本文前,建议先掌握/回顾 《5 · 文本清洗:字符串高阶操作与正则》 中的核心概念;本文会在这个基础上继续推进。 承上启下:上一篇我们用正则清洗了文本。现在进入函数的深水区——装饰器是 Python 最优雅的特性之一,Flask/FastAPI 的路由、pytest 的 fixture 都基于它。
极客解析:先把数据流、控制流和模块边界跑通,再谈抽象;每段代码都围绕一个可执行 CLI 闭环展开。
痛点与架构
装饰器的本质:用一个函数包装另一个函数,不修改原函数代码,扩展其行为。
原函数 fn
↓ decorator(fn)
wrapper 函数(新功能 + 调用原函数)
↓ @decorator 语法糖
fn = decorator(fn) ← 自动完成这一步
| 概念 | 一句话 | 典型用途 |
|---|---|---|
| 闭包 | 函数记住了它定义时的环境 | 计数器、缓存、工厂函数 |
| 装饰器 | 包装函数,扩展行为 | 计时、重试、权限、缓存 |
| 装饰器工厂 | 带参数的装饰器 | @retry(max=3) |
实战演练场
步步为营:核心逻辑自适应拆解
装饰器不是魔法,它就是“把一个函数交给另一个函数包装”。这一篇我们按“闭包记忆 -> 计时包装 -> 重试包装 -> 缓存包装 -> 业务函数组合 -> 性能报告”的顺序慢慢拆。
Step 1:用闭包做一个会记住上次调用时间的限速器
痛点与机制:
闭包最难的点是“函数为什么还能记住外面的变量”。你可以把 make_rate_limiter() 想成做了一个带秒表的保安,last_call_time 就是他手里的小纸条;每次有人进门,他先看看上次是什么时候放行的。
核心源码(逐字来自文末完整源码):
def make_rate_limiter(calls_per_second: float) -> Callable:
"""
闭包工厂:创建一个限速器。
interval 变量被内部函数 limiter 捕获——这就是闭包。
"""
interval = 1.0 / calls_per_second
last_call_time: list[float] = [0.0] # 用列表包装,允许 nonlocal 修改
def limiter(fn: Callable) -> Callable:
@functools.wraps(fn)
def wrapper(*args: Any, **kwargs: Any) -> Any:
elapsed = time.time() - last_call_time[0]
if elapsed < interval:
time.sleep(interval - elapsed)
last_call_time[0] = time.time()
return fn(*args, **kwargs)
return wrapper
return limiter
可运行演示(补齐 Mock 数据与 print 反馈):
import functools
import random
import time
from collections import defaultdict
from typing import Any, Callable, TypeVar
F = TypeVar("F", bound=Callable[..., Any])
_perf_records: dict[str, list[float]] = defaultdict(list)
def make_rate_limiter(calls_per_second: float) -> Callable:
"""
闭包工厂:创建一个限速器。
interval 变量被内部函数 limiter 捕获——这就是闭包。
"""
interval = 1.0 / calls_per_second
last_call_time: list[float] = [0.0] # 用列表包装,允许 nonlocal 修改
def limiter(fn: Callable) -> Callable:
@functools.wraps(fn)
def wrapper(*args: Any, **kwargs: Any) -> Any:
elapsed = time.time() - last_call_time[0]
if elapsed < interval:
time.sleep(interval - elapsed)
last_call_time[0] = time.time()
return fn(*args, **kwargs)
return wrapper
return limiter
@make_rate_limiter(5.0)
def fetch_task(task_id: int) -> str:
return f"任务 {task_id} 已拉取"
start = time.perf_counter()
for i in range(3):
print(fetch_task(i))
print(f"总耗时约: {time.perf_counter() - start:.2f} 秒")
Step 2:用 timer 给函数套一层秒表,不改原函数也能计时
痛点与机制:
@timer 的本质是 slow_add = timer(slow_add)。原函数像一杯咖啡,装饰器像给杯子套上隔热套:咖啡没变,但你拿它时多了“记录耗时”这个能力。
核心源码(逐字来自文末完整源码):
def timer(fn: F) -> F:
"""记录函数每次调用的耗时,存入全局性能记录。"""
@functools.wraps(fn)
def wrapper(*args: Any, **kwargs: Any) -> Any:
start = time.perf_counter()
result = fn(*args, **kwargs)
elapsed = time.perf_counter() - start
_perf_records[fn.__name__].append(elapsed)
return result
return wrapper # type: ignore[return-value]
可运行演示(补齐 Mock 数据与 print 反馈):
import functools
import random
import time
from collections import defaultdict
from typing import Any, Callable, TypeVar
F = TypeVar("F", bound=Callable[..., Any])
_perf_records: dict[str, list[float]] = defaultdict(list)
# Step 2:装饰器像给函数套透明外壳,先计时,再调用原函数。
def timer(fn: F) -> F:
"""记录函数每次调用的耗时,存入全局性能记录。"""
@functools.wraps(fn)
def wrapper(*args: Any, **kwargs: Any) -> Any:
start = time.perf_counter()
result = fn(*args, **kwargs)
elapsed = time.perf_counter() - start
_perf_records[fn.__name__].append(elapsed)
return result
return wrapper # type: ignore[return-value]
@timer
def slow_add(a: int, b: int) -> int:
time.sleep(0.03)
return a + b
print("计算结果:", slow_add(3, 5))
print("性能记录:", {k: [round(x, 4) for x in v] for k, v in _perf_records.items()})
Step 3:用 retry 装饰器工厂把“失败重试”参数化
痛点与机制:
retry(max_attempts=3, delay=0.01) 先把“最多试几次、隔多久再试”记下来,再生成真正包函数的装饰器。它像客服回拨机制:第一次没人接不会马上放弃,而是按规则再拨几次。
核心源码(逐字来自文末完整源码):
def retry(max_attempts: int = 3, delay: float = 0.1, exceptions: tuple = (Exception,)):
"""
带参数的装饰器(装饰器工厂)。
用法:@retry(max_attempts=3, delay=0.5)
"""
def decorator(fn: F) -> F:
@functools.wraps(fn)
def wrapper(*args: Any, **kwargs: Any) -> Any:
last_exc: Exception | None = None
for attempt in range(1, max_attempts + 1):
try:
return fn(*args, **kwargs)
except exceptions as e:
last_exc = e
if attempt < max_attempts:
print(f" ⚠️ {fn.__name__} 第{attempt}次失败: {e},{delay}s后重试")
time.sleep(delay)
raise RuntimeError(f"{fn.__name__} 重试{max_attempts}次后仍失败") from last_exc
return wrapper # type: ignore[return-value]
return decorator
可运行演示(补齐 Mock 数据与 print 反馈):
import functools
import random
import time
from collections import defaultdict
from typing import Any, Callable, TypeVar
F = TypeVar("F", bound=Callable[..., Any])
_perf_records: dict[str, list[float]] = defaultdict(list)
# Step 3:装饰器工厂先接收配置,再返回真正的装饰器。
def retry(max_attempts: int = 3, delay: float = 0.1, exceptions: tuple = (Exception,)):
"""
带参数的装饰器(装饰器工厂)。
用法:@retry(max_attempts=3, delay=0.5)
"""
def decorator(fn: F) -> F:
@functools.wraps(fn)
def wrapper(*args: Any, **kwargs: Any) -> Any:
last_exc: Exception | None = None
for attempt in range(1, max_attempts + 1):
try:
return fn(*args, **kwargs)
except exceptions as e:
last_exc = e
if attempt < max_attempts:
print(f" ⚠️ {fn.__name__} 第{attempt}次失败: {e},{delay}s后重试")
time.sleep(delay)
raise RuntimeError(f"{fn.__name__} 重试{max_attempts}次后仍失败") from last_exc
return wrapper # type: ignore[return-value]
return decorator
attempts = {"count": 0}
@retry(max_attempts=3, delay=0.01, exceptions=(ConnectionError,))
def flaky_service() -> str:
attempts["count"] += 1
if attempts["count"] < 3:
raise ConnectionError("临时网络抖动")
return "第三次终于成功"
print("调用结果:", flaky_service())
print("实际尝试次数:", attempts["count"])
Step 4:用 simple_cache 记住算过的结果,少做重复劳动
痛点与机制:
缓存的核心是“相同输入直接复用旧答案”。第一次算 square(4) 要真的计算,第二次再来 4 就像翻备忘录,速度更快。hits 是命中缓存,misses 是没找到只能重算。
核心源码(逐字来自文末完整源码):
def simple_cache(maxsize: int = 128):
"""简化版 LRU 缓存装饰器,演示缓存原理。"""
def decorator(fn: F) -> F:
cache: dict[tuple, Any] = {}
hits = misses = 0
@functools.wraps(fn)
def wrapper(*args: Any) -> Any:
nonlocal hits, misses
key = args
if key in cache:
hits += 1
return cache[key]
misses += 1
result = fn(*args)
if len(cache) >= maxsize:
# 删除最旧的条目
oldest_key = next(iter(cache))
del cache[oldest_key]
cache[key] = result
return result
def cache_info() -> str:
return f"hits={hits}, misses={misses}, size={len(cache)}"
wrapper.cache_info = cache_info # type: ignore[attr-defined]
return wrapper # type: ignore[return-value]
return decorator
可运行演示(补齐 Mock 数据与 print 反馈):
import functools
import random
import time
from collections import defaultdict
from typing import Any, Callable, TypeVar
F = TypeVar("F", bound=Callable[..., Any])
_perf_records: dict[str, list[float]] = defaultdict(list)
# Step 4:缓存像备忘录,见过的入参直接查表,不再重新计算。
def simple_cache(maxsize: int = 128):
"""简化版 LRU 缓存装饰器,演示缓存原理。"""
def decorator(fn: F) -> F:
cache: dict[tuple, Any] = {}
hits = misses = 0
@functools.wraps(fn)
def wrapper(*args: Any) -> Any:
nonlocal hits, misses
key = args
if key in cache:
hits += 1
return cache[key]
misses += 1
result = fn(*args)
if len(cache) >= maxsize:
# 删除最旧的条目
oldest_key = next(iter(cache))
del cache[oldest_key]
cache[key] = result
return result
def cache_info() -> str:
return f"hits={hits}, misses={misses}, size={len(cache)}"
wrapper.cache_info = cache_info # type: ignore[attr-defined]
return wrapper # type: ignore[return-value]
return decorator
@simple_cache(maxsize=2)
def square(n: int) -> int:
print(f"真正计算: {n} * {n}")
return n * n
for value in [4, 4, 5, 4]:
print(f"square({value}) =", square(value))
print("缓存状态:", square.cache_info())
Step 5:把 timer 用到真实批处理函数上,自动收集耗时
痛点与机制:
这里开始进入“业务函数 + 装饰器”的组合。process_text_batch() 只关心处理文本批次,@timer 负责在外层悄悄记录耗时。新手可以先记住:装饰器负责横切能力,业务函数负责核心任务。
核心源码(逐字来自文末完整源码):
@timer
def process_text_batch(batch_id: int, size: int = 100) -> dict[str, int]:
"""模拟文本批处理任务。"""
time.sleep(random.uniform(0.01, 0.05)) # 模拟 IO 耗时
return {"batch_id": batch_id, "processed": size, "errors": random.randint(0, 3)}
可运行演示(补齐 Mock 数据与 print 反馈):
import functools
import random
import time
from collections import defaultdict
from typing import Any, Callable, TypeVar
F = TypeVar("F", bound=Callable[..., Any])
_perf_records: dict[str, list[float]] = defaultdict(list)
def timer(fn: F) -> F:
"""记录函数每次调用的耗时,存入全局性能记录。"""
@functools.wraps(fn)
def wrapper(*args: Any, **kwargs: Any) -> Any:
start = time.perf_counter()
result = fn(*args, **kwargs)
elapsed = time.perf_counter() - start
_perf_records[fn.__name__].append(elapsed)
return result
return wrapper # type: ignore[return-value]
# Step 5:业务函数只写业务,耗时记录交给 @timer。
@timer
def process_text_batch(batch_id: int, size: int = 100) -> dict[str, int]:
"""模拟文本批处理任务。"""
time.sleep(random.uniform(0.01, 0.05)) # 模拟 IO 耗时
return {"batch_id": batch_id, "processed": size, "errors": random.randint(0, 3)}
random.seed(7)
for batch_id in range(3):
print("批处理结果:", process_text_batch(batch_id, size=50))
print("已记录耗时次数:", len(_perf_records["process_text_batch"]))
Step 6:把 retry 和 timer 叠起来,处理不稳定接口
痛点与机制:
两个装饰器叠放时,执行关系像穿衣服:先穿里面的 @timer,再穿外面的 @retry。接口失败时,外层 retry 会接住异常并重试;只要某一次调用成功返回,内层 timer 就能把这次成功调用的耗时记录下来。
核心源码(逐字来自文末完整源码):
@retry(max_attempts=3, delay=0.05, exceptions=(ConnectionError,))
@timer
def unstable_api_call(endpoint: str) -> str:
"""模拟不稳定的 API 调用(70% 概率失败)。"""
if random.random() < 0.7:
raise ConnectionError(f"连接 {endpoint} 超时")
return f"✅ {endpoint} 响应成功"
可运行演示(补齐 Mock 数据与 print 反馈):
import functools
import random
import time
from collections import defaultdict
from typing import Any, Callable, TypeVar
F = TypeVar("F", bound=Callable[..., Any])
_perf_records: dict[str, list[float]] = defaultdict(list)
def timer(fn: F) -> F:
"""记录函数每次调用的耗时,存入全局性能记录。"""
@functools.wraps(fn)
def wrapper(*args: Any, **kwargs: Any) -> Any:
start = time.perf_counter()
result = fn(*args, **kwargs)
elapsed = time.perf_counter() - start
_perf_records[fn.__name__].append(elapsed)
return result
return wrapper # type: ignore[return-value]
def retry(max_attempts: int = 3, delay: float = 0.1, exceptions: tuple = (Exception,)):
"""
带参数的装饰器(装饰器工厂)。
用法:@retry(max_attempts=3, delay=0.5)
"""
def decorator(fn: F) -> F:
@functools.wraps(fn)
def wrapper(*args: Any, **kwargs: Any) -> Any:
last_exc: Exception | None = None
for attempt in range(1, max_attempts + 1):
try:
return fn(*args, **kwargs)
except exceptions as e:
last_exc = e
if attempt < max_attempts:
print(f" ⚠️ {fn.__name__} 第{attempt}次失败: {e},{delay}s后重试")
time.sleep(delay)
raise RuntimeError(f"{fn.__name__} 重试{max_attempts}次后仍失败") from last_exc
return wrapper # type: ignore[return-value]
return decorator
# Step 6:多个装饰器像多层防护服,离函数最近的 @timer 先包,再由 @retry 包外层。
@retry(max_attempts=3, delay=0.05, exceptions=(ConnectionError,))
@timer
def unstable_api_call(endpoint: str) -> str:
"""模拟不稳定的 API 调用(70% 概率失败)。"""
if random.random() < 0.7:
raise ConnectionError(f"连接 {endpoint} 超时")
return f"✅ {endpoint} 响应成功"
# seed=1 的随机序列会先失败、再成功,方便你看清“重试后成功”的全过程。
random.seed(1)
try:
print("接口结果:", unstable_api_call("/api/v1/tasks"))
except RuntimeError as exc:
print("接口最终失败:", exc)
print("性能记录函数名:", list(_perf_records.keys()))
Step 7:给耗时的向量计算加缓存,让重复文本秒回
痛点与机制:
机器学习里的向量计算通常很贵。这里用 time.sleep(0.02) 模拟慢操作,再用缓存避免重复算同一个文本。它像饭店常客点同一份套餐,厨房不用每次重新研究菜单,直接按熟单出餐。
核心源码(逐字来自文末完整源码):
@simple_cache(maxsize=32)
@timer
def compute_embedding(text: str) -> list[float]:
"""模拟计算文本向量(耗时操作,适合缓存)。"""
time.sleep(0.02) # 模拟模型推理耗时
return [hash(c) % 100 / 100 for c in text[:4]]
可运行演示(补齐 Mock 数据与 print 反馈):
import functools
import random
import time
from collections import defaultdict
from typing import Any, Callable, TypeVar
F = TypeVar("F", bound=Callable[..., Any])
_perf_records: dict[str, list[float]] = defaultdict(list)
def timer(fn: F) -> F:
"""记录函数每次调用的耗时,存入全局性能记录。"""
@functools.wraps(fn)
def wrapper(*args: Any, **kwargs: Any) -> Any:
start = time.perf_counter()
result = fn(*args, **kwargs)
elapsed = time.perf_counter() - start
_perf_records[fn.__name__].append(elapsed)
return result
return wrapper # type: ignore[return-value]
def simple_cache(maxsize: int = 128):
"""简化版 LRU 缓存装饰器,演示缓存原理。"""
def decorator(fn: F) -> F:
cache: dict[tuple, Any] = {}
hits = misses = 0
@functools.wraps(fn)
def wrapper(*args: Any) -> Any:
nonlocal hits, misses
key = args
if key in cache:
hits += 1
return cache[key]
misses += 1
result = fn(*args)
if len(cache) >= maxsize:
# 删除最旧的条目
oldest_key = next(iter(cache))
del cache[oldest_key]
cache[key] = result
return result
def cache_info() -> str:
return f"hits={hits}, misses={misses}, size={len(cache)}"
wrapper.cache_info = cache_info # type: ignore[attr-defined]
return wrapper # type: ignore[return-value]
return decorator
# Step 7:向量计算假装是模型推理,同一句文本重复出现时最适合缓存。
@simple_cache(maxsize=32)
@timer
def compute_embedding(text: str) -> list[float]:
"""模拟计算文本向量(耗时操作,适合缓存)。"""
time.sleep(0.02) # 模拟模型推理耗时
return [hash(c) % 100 / 100 for c in text[:4]]
for text in ["python", "python", "decorator", "python"]:
print(f"{text} ->", compute_embedding(text))
print("缓存状态:", compute_embedding.cache_info())
print("计时记录次数:", len(_perf_records["compute_embedding"]))
Step 8:把所有性能记录汇总成终端报告
痛点与机制:
print_perf_report() 是最后的仪表盘。前面的装饰器一直在后台记账,这里把调用次数、总耗时、平均耗时和峰值一次性列出来。对新手来说,这一步能把“装饰器做了什么”变成看得见的表格。
核心源码(逐字来自文末完整源码):
def print_perf_report() -> None:
if not _perf_records:
print(" 暂无性能数据")
return
print(f"\n {'函数名':<25} {'调用':<6} {'总耗时':<10} {'均值':<10} {'峰值'}")
print(f" {'─'*25} {'─'*6} {'─'*10} {'─'*10} {'─'*10}")
for name, times in sorted(_perf_records.items()):
total = sum(times)
avg = total / len(times)
peak = max(times)
print(f" {name:<25} {len(times):<6} {total:<10.4f} {avg:<10.4f} {peak:.4f}")
可运行演示(补齐 Mock 数据与 print 反馈):
import functools
import random
import time
from collections import defaultdict
from typing import Any, Callable, TypeVar
F = TypeVar("F", bound=Callable[..., Any])
_perf_records: dict[str, list[float]] = defaultdict(list)
# Step 8:报告函数只读 _perf_records,把零散耗时整理成表格。
def print_perf_report() -> None:
if not _perf_records:
print(" 暂无性能数据")
return
print(f"\n {'函数名':<25} {'调用':<6} {'总耗时':<10} {'均值':<10} {'峰值'}")
print(f" {'─'*25} {'─'*6} {'─'*10} {'─'*10} {'─'*10}")
for name, times in sorted(_perf_records.items()):
total = sum(times)
avg = total / len(times)
peak = max(times)
print(f" {name:<25} {len(times):<6} {total:<10.4f} {avg:<10.4f} {peak:.4f}")
_perf_records["load_data"].extend([0.02, 0.03, 0.01])
_perf_records["train_model"].extend([0.12, 0.15])
print_perf_report()
极客实战:完整源码与运行
现在,把上面的积木拼起来,将以下完整代码放进你的编辑器,运行它。先看整体闭环,再回头逐段改参数,你会更容易建立工程直觉。
# fn_probe.py
"""
函数性能探针工具链 —— 演示闭包、装饰器、装饰器工厂。
用法:
python3 fn_probe.py
python3 fn_probe.py --mode timer
python3 fn_probe.py --mode retry
python3 fn_probe.py --mode cache
"""
import argparse
import functools
import time
import random
from collections import defaultdict
from typing import Any, Callable, TypeVar
F = TypeVar("F", bound=Callable[..., Any])
# ── 全局性能记录(闭包共享状态)─────────────────────────────
_perf_records: dict[str, list[float]] = defaultdict(list)
# ── 1. 闭包:函数工厂 ─────────────────────────────────────────
def make_rate_limiter(calls_per_second: float) -> Callable:
"""
闭包工厂:创建一个限速器。
interval 变量被内部函数 limiter 捕获——这就是闭包。
"""
interval = 1.0 / calls_per_second
last_call_time: list[float] = [0.0] # 用列表包装,允许 nonlocal 修改
def limiter(fn: Callable) -> Callable:
@functools.wraps(fn)
def wrapper(*args: Any, **kwargs: Any) -> Any:
elapsed = time.time() - last_call_time[0]
if elapsed < interval:
time.sleep(interval - elapsed)
last_call_time[0] = time.time()
return fn(*args, **kwargs)
return wrapper
return limiter
# ── 2. 基础装饰器:计时器 ─────────────────────────────────────
def timer(fn: F) -> F:
"""记录函数每次调用的耗时,存入全局性能记录。"""
@functools.wraps(fn)
def wrapper(*args: Any, **kwargs: Any) -> Any:
start = time.perf_counter()
result = fn(*args, **kwargs)
elapsed = time.perf_counter() - start
_perf_records[fn.__name__].append(elapsed)
return result
return wrapper # type: ignore[return-value]
# ── 3. 装饰器工厂:重试 ───────────────────────────────────────
def retry(max_attempts: int = 3, delay: float = 0.1, exceptions: tuple = (Exception,)):
"""
带参数的装饰器(装饰器工厂)。
用法:@retry(max_attempts=3, delay=0.5)
"""
def decorator(fn: F) -> F:
@functools.wraps(fn)
def wrapper(*args: Any, **kwargs: Any) -> Any:
last_exc: Exception | None = None
for attempt in range(1, max_attempts + 1):
try:
return fn(*args, **kwargs)
except exceptions as e:
last_exc = e
if attempt < max_attempts:
print(f" ⚠️ {fn.__name__} 第{attempt}次失败: {e},{delay}s后重试")
time.sleep(delay)
raise RuntimeError(f"{fn.__name__} 重试{max_attempts}次后仍失败") from last_exc
return wrapper # type: ignore[return-value]
return decorator
# ── 4. 装饰器工厂:LRU 缓存(手动实现原理)──────────────────
def simple_cache(maxsize: int = 128):
"""简化版 LRU 缓存装饰器,演示缓存原理。"""
def decorator(fn: F) -> F:
cache: dict[tuple, Any] = {}
hits = misses = 0
@functools.wraps(fn)
def wrapper(*args: Any) -> Any:
nonlocal hits, misses
key = args
if key in cache:
hits += 1
return cache[key]
misses += 1
result = fn(*args)
if len(cache) >= maxsize:
# 删除最旧的条目
oldest_key = next(iter(cache))
del cache[oldest_key]
cache[key] = result
return result
def cache_info() -> str:
return f"hits={hits}, misses={misses}, size={len(cache)}"
wrapper.cache_info = cache_info # type: ignore[attr-defined]
return wrapper # type: ignore[return-value]
return decorator
# ── 被装饰的业务函数 ──────────────────────────────────────────
@timer
def process_text_batch(batch_id: int, size: int = 100) -> dict[str, int]:
"""模拟文本批处理任务。"""
time.sleep(random.uniform(0.01, 0.05)) # 模拟 IO 耗时
return {"batch_id": batch_id, "processed": size, "errors": random.randint(0, 3)}
@retry(max_attempts=3, delay=0.05, exceptions=(ConnectionError,))
@timer
def unstable_api_call(endpoint: str) -> str:
"""模拟不稳定的 API 调用(70% 概率失败)。"""
if random.random() < 0.7:
raise ConnectionError(f"连接 {endpoint} 超时")
return f"✅ {endpoint} 响应成功"
@simple_cache(maxsize=32)
@timer
def compute_embedding(text: str) -> list[float]:
"""模拟计算文本向量(耗时操作,适合缓存)。"""
time.sleep(0.02) # 模拟模型推理耗时
return [hash(c) % 100 / 100 for c in text[:4]]
# ── 性能报告 ──────────────────────────────────────────────────
def print_perf_report() -> None:
if not _perf_records:
print(" 暂无性能数据")
return
print(f"\n {'函数名':<25} {'调用':<6} {'总耗时':<10} {'均值':<10} {'峰值'}")
print(f" {'─'*25} {'─'*6} {'─'*10} {'─'*10} {'─'*10}")
for name, times in sorted(_perf_records.items()):
total = sum(times)
avg = total / len(times)
peak = max(times)
print(f" {name:<25} {len(times):<6} {total:<10.4f} {avg:<10.4f} {peak:.4f}")
def demo_timer() -> None:
print("\n ── 计时装饰器演示 ────────────────────────")
for i in range(5):
result = process_text_batch(i, size=200)
print(f" Batch {i}: {result}")
print_perf_report()
def demo_retry() -> None:
print("\n ── 重试装饰器演示 ────────────────────────")
random.seed(42)
try:
result = unstable_api_call("/api/v1/tasks")
print(f" 最终结果: {result}")
except RuntimeError as e:
print(f" 最终失败: {e}")
def demo_cache() -> None:
print("\n ── 缓存装饰器演示 ────────────────────────")
texts = ["python", "machine learning", "python", "deep learning", "python"]
for text in texts:
vec = compute_embedding(text)
print(f" '{text}' → {vec[:3]}...")
print(f"\n 缓存状态: {compute_embedding.cache_info()}") # type: ignore[attr-defined]
def main() -> None:
parser = argparse.ArgumentParser(description="函数性能探针工具链")
parser.add_argument("--mode", choices=["timer", "retry", "cache", "all"], default="all")
args = parser.parse_args()
if args.mode in ("timer", "all"):
demo_timer()
if args.mode in ("retry", "all"):
demo_retry()
if args.mode in ("cache", "all"):
demo_cache()
if __name__ == "__main__":
main()
终端预期输出(--mode cache):
$ python3 fn_probe.py --mode cache
── 缓存装饰器演示 ────────────────────────
'python' → [0.12, 0.34, 0.56]...
'machine learning' → [0.45, 0.23, 0.67]...
'python' → [0.12, 0.34, 0.56]... ← 命中缓存,无延迟
'deep learning' → [0.78, 0.12, 0.34]...
'python' → [0.12, 0.34, 0.56]... ← 命中缓存
缓存状态: hits=2, misses=3, size=3
装饰器执行顺序
# 装饰器叠加顺序演示
def decorator_a(fn):
def wrapper(*a, **kw):
print("A before"); result = fn(*a, **kw); print("A after"); return result
return wrapper
def decorator_b(fn):
def wrapper(*a, **kw):
print("B before"); result = fn(*a, **kw); print("B after"); return result
return wrapper
@decorator_a
@decorator_b
def fn(): print("fn()")
# 等价于:fn = decorator_a(decorator_b(fn))
fn()
NexDo Time ⚡
5 分钟极客微操:给 timer 装饰器增加 threshold 参数(变成装饰器工厂),当某次调用耗时超过阈值时,自动打印警告:⚠️ process_text_batch 耗时 0.08s,超过阈值 0.05s。
Don’t wait for next time, do it in the next moment.