ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python函数追踪与测试替换:Wrapture库实战指南

Python函数追踪与测试替换:Wrapture库实战指南 1. 从“包装一个函数”说起Wrapture 的背景与定位1.1 为什么需要 Wrapture在日常 Python 开发中我们经常会碰到一类需求在不修改原函数代码的前提下给函数增加额外的逻辑。举几个最常见的例子记录某个函数的调用时间、入参和返回值用来排查性能问题在函数调用前后自动打印日志方便定位线上问题在测试环境中临时替换某个函数的实现比如把第三方接口返回固定数据给已有函数统一增加重试、熔断、权限校验等通用能力。这些场景本质上都指向同一个动作——包装函数。Python 里实现函数包装的主要手段是“装饰器”但装饰器在使用过程中也有很多讲究如何保留被装饰函数的元信息如何正确传递不定长参数如何让装饰器既能用于普通函数又能用于类方法、静态方法这些问题如果每次都重复实现很容易踩坑。Graham Dumpleton 发布了 Python 库 Wrapture目标就是让这类“函数包装、追踪、替换”的工作更加简单和统一。该作者在 Python 生态中本身就是装饰器与包装领域的资深开发者因此 Wrapture 的设计会重点关注“包装透明性”和“调试友好性”。简单理解Wrapture 解决的问题就是让开发者以更少的样板代码安全地包装 Python 函数并在开发或测试阶段灵活替换函数行为。1.2 Wrapture 适合谁使用下面几类开发者使用 Wrapture 的收益最大正在开发 Python 库或框架需要给用户提供可插拔的扩展机制在大型项目中定位线上问题希望快速确认某个函数被谁调用、传入了什么参数、返回了什么结果编写单元测试需要替换外部接口或耗时操作想要深入理解 Python 装饰器、描述符、函数签名传递等机制。如果你目前正在学习 Python 基础语法还不太理解“装饰器”“闭包”这些概念可以先掌握基础后再来看 Wrapture。如果你的项目里已经有大量手写的装饰器代码并且经常出现“装饰后函数信息丢失”“多层装饰参数传递错误”等问题那这篇文章的内容会非常有帮助。1.3 核心能力功能地图围绕“函数追踪与测试替换”这两个关键词本文将从最底层的装饰器原理讲起再逐步拆解 Wrapture 这类库带来的便利能力。整个内容分四层Python 函数包装的基础知识理解装饰器为什么能“包装”。函数追踪调用前、调用后、异常时的钩子设计。测试替换在不修改源码的情况下替换函数行为。工程实践命名、错误处理、日志、安全边界。下面我们先完成环境准备然后用可运行的代码一步步展开。2. 环境准备与版本说明2.1 Python 环境准备Wrapture 是 Python 库因此你首先需要一个可用的 Python 环境。如果你已经安装过 Python可以在终端中使用下面命令确认版本python --version # 或者 python3 --version如果还没有安装 Python建议到 Python 官方网站下载安装包。安装完成后建议顺手验证 pip 是否可用pip --version版本方面不需要刻意追求最新。Wrapture 这类包装库通常对 Python 3.7 以上版本的支持都比较好但具体支持范围要以官方文档为准。本文以下示例使用 Python 3.10 环境演示如果你的环境略有差异注意把代码中的类型注解示例与旧版本语法做匹配即可。2.2 安装 WraptureWrapture 的安装方式与大多数 Python 库一致通过 pip 安装pip install wrapture如果你的项目使用了虚拟环境请确保已经激活虚拟环境再执行安装。# 虚拟环境创建示例 python -m venv venv # Windows 激活 venv\Scripts\activate # macOS / Linux 激活 source venv/bin/activate激活虚拟环境后再执行 pip install 命令。这样可以把依赖隔离在项目内部避免污染全局 Python 环境。2.3 验证安装结果安装完成后进入 Python 交互式环境或新建一个 Python 文件尝试导入 Wraptureimport wrapture print(wrapture.__file__)如果输出的是一个文件路径说明安装成功。如果出现 ModuleNotFoundError通常有以下几个原因原因判断方式解决思路没有安装成功执行 pip install 后出现报错根据报错信息补充依赖或切换 Python 版本安装到了错误环境pip 与 python 不是同一个环境在激活的虚拟环境中重新安装Python 环境混乱系统存在多个 Python使用 python -m pip install 代替 pip install为了保险推荐使用python -m pip install wrapture方式安装。2.4 本文的示例项目结构后续所有示例代码建议放在一个项目中方便运行和测试。目录结构如下wrapture-demo/ ├── venv/ # 虚拟环境可选 ├── call_tracker.py # 追踪器示例代码 ├── test_replace_demo.py # 测试替换示例代码 └── README.md # 笔记说明可选在实际开发中建议按模块拆分而不是把所有代码堆在一个文件里。本文为了演示方便会给出每个文件的完整代码。3. 函数追踪与测试替换的原理拆解3.1 Python 包装器基础回顾要想理解 Wrapture 这类库先要理解 Python 里函数装饰器的基础原理。先看一个最简单的装饰器import time def cost_time(func): def wrapper(*args, **kwargs): start time.perf_counter() result func(*args, **kwargs) end time.perf_counter() print(f函数 {func.__name__} 耗时 {end - start:.6f} 秒) return result return wrapper cost_time def add(a, b): time.sleep(0.1) return a b if __name__ __main__: print(add(1, 2))运行这段代码会输出函数 add 耗时 0.100xxx 秒 3这个示例的关键点在于cost_time本质上执行了add cost_time(add)。也就是说add这个名字不再指向原来的求和函数而是指向wrapper这个新函数。后续调用add(1, 2)时实际执行的是wrapper(1, 2)。这种机制是后面所有“函数追踪”与“测试替换”能力的基础。3.2 包装器的常见痛点上面的简单装饰器可以实现功能但工程中会暴露一些问题问题一函数元信息丢失。print(add.__name__) # 输出 wrapper而不是 add如果越来越多的装饰器叠加会导致函数名、注释、参数签名都变得不可读最终影响调试和接口文档生成。问题二类方法场景更复杂。普通函数装饰器直接套用到类方法上有时会出现self参数处理不当的情况。例如装饰器里调用func(*args, **kwargs)时args的第一个元素通常是实例对象self需要小心处理。问题三多层装饰协作困难。当函数上叠加了缓存、鉴权、日志等多个装饰器时如果每个装饰器各自实现很容易出现参数丢失、执行顺序混乱、异常被吞掉等问题。3.3 “函数追踪”的设计思路在 Wrapture 这一类库中函数追踪通常拆成以下几个阶段调用前可以观察函数名、入参、调用者信息。调用后可以观察返回值、耗时、是否成功。异常时可以捕获异常信息记录堆栈再决定向上抛出还是吞掉。我们可以用 Python 的functools.wraps和闭包来实现一个可复用的追踪器先不看 Wrapture 的具体 API理解思想更为重要。import functools import time import inspect def trace_call(func): functools.wraps(func) def wrapper(*args, **kwargs): # 调用前 print(f[CALL] {func.__module__}.{func.__qualname__}) print(f[ARGS] args{args}, kwargs{kwargs}) start time.perf_counter() try: result func(*args, **kwargs) except Exception as exc: # 异常时 print(f[ERROR] {type(exc).__name__}: {exc}) raise else: # 调用后 elapsed time.perf_counter() - start print(f[RETURN] {result!r}, cost{elapsed:.4f}s) return result return wrapper trace_call def divide(a, b): return a / b if __name__ __main__: divide(10, 2)这段代码演示了一个追踪器应该具备的切面能力。Wrapture 这类库的实际价值是把这种“通用切面逻辑”抽象为可配置、可组合的高级工具让使用者不需要每次手写。3.4 “测试替换”的设计思路测试替换最常见的需求是在测试中把某个真实函数换成桩函数或模拟对象。Python 标准库提供了unittest.mock.patch例如from unittest.mock import patch def fetch_user_name(user_id): # 假装这是联网或查数据库的函数 raise NotImplementedError(真实环境不可用) def greet_user(user_id): name fetch_user_name(user_id) return fHello, {name} def test_greet_user(): with patch(__main__.fetch_user_name, return_valueAlice): print(greet_user(1))patch的核心也是在运行期“替换”了模块内的全局名字。这种做法对测试非常有用。Wrapture 类库通常也会围绕“命名空间替换”“对象方法替换”等场景提供更统一的入口并确保替换时不会破坏对象的原有类型关系和描述符协议。3.5 Wrapture 带来的简化方向回到 Wrapture 本身。虽然目前详细 API 文档需要以官方发布版本为准但从库的目标和同类设计来看它通常会在以下几个方面简化开发提供统一的包装器基类让开发者可以只实现自己关心的钩子方法自动处理函数元信息保留减少functools.wraps的重复使用对普通函数、类方法、静态方法、类方法提供一致包装在追踪信息中提供更清晰的上下文比如“哪个文件哪一行发起了调用”测试替换时支持按条件替换也方便在测试结束后自动还原。因此建议你把 Wrapture 理解为一个“更高级的包装基础设施”而不是一个单一功能的装饰器库。4. 完整实战案例用可运行代码演示函数追踪这一节我们来做一个能实际运行的追踪示例。为了保证代码不依赖某个尚不明确的版本 API这里会先使用 Python 标准方式实现一个轻量追踪器再预留通过 Wrapture 实现的演进空间。如果你在本地已经安装了 Wrapture可以参考官方文档中的装饰器写法做替换。4.1 创建项目结构进入工作目录创建文件call_tracker.pymkdir wrapture-demo cd wrapture-demo touch call_tracker.py4.2 编写追踪器核心代码打开call_tracker.py写入以下内容# 文件路径wrapture-demo/call_tracker.py import functools import time import inspect from typing import Any, Callable, Dict class CallTracker: 一个简单的调用追踪器用来记录函数调用信息。 def __init__(self, log_enabled: bool True): self.log_enabled log_enabled self.records [] def _log(self, message: str) - None: if self.log_enabled: print(message) def track(self, func: Callable) - Callable: 将某个函数包装为可追踪函数。 functools.wraps(func) def wrapper(*args, **kwargs): record { func_name: func.__qualname__, args: args, kwargs: kwargs, caller: inspect.stack()[1].function, start_time: time.perf_counter(), end_time: None, return_value: None, exception: None, } self._log(f[TRACE] 调用 {func.__qualname__}) self._log(f[ARGS] args{args}, kwargs{kwargs}) self._log(f[CALLER] 调用方{record[caller]}) try: result func(*args, **kwargs) record[return_value] result return result except Exception as exc: record[exception] exc self._log(f[ERROR] {type(exc).__name__}: {exc}) raise finally: record[end_time] time.perf_counter() record[cost] record[end_time] - record[start_time] self.records.append(record) self._log(f[COST] {record[cost]:.6f} 秒) return wrapper def report(self) - None: 打印所有记录。 for idx, record in enumerate(self.records, 1): print(f{idx}. {record[func_name]} - 返回值: {record[return_value]} - f耗时: {record.get(cost, 0):.6f}s) tracker CallTracker() tracker.track def add(a: int, b: int) - int: 两数相加。 return a b tracker.track def fetch_data(url: str) - Dict[str, Any]: 模拟请求外部数据。 time.sleep(0.2) return {status: 200, url: url} if __name__ __main__: add(1, 2) add(10, 20) fetch_data(https://api.example.com/users) print(\n 追踪报告 ) tracker.report()4.3 运行与验证在终端执行python call_tracker.py预期输出类似下面这样[TRACE] 调用 add [ARGS] args(1, 2), kwargs{} [CALLER] 调用方module [COST] 0.000012 秒 [TRACE] 调用 add [ARGS] args(10, 20), kwargs{} [CALLER] 调用方module [COST] 0.000003 秒 [TRACE] 调用 fetch_data [ARGS] args(https://api.example.com/users,), kwargs{} [CALLER] 调用方module [COST] 0.200123 秒 追踪报告 1. add - 返回值: 3 - 耗时: 0.000012s 2. add - 返回值: 30 - 耗时: 0.000003s 3. fetch_data - 返回值: {status: 200, url: https://api.example.com/users} - 耗时: 0.200123s4.4 代码设计说明这段代码虽然不复杂但包含了追踪器的几个关键设计点使用functools.wraps(func)保留函数元信息这样add.__name__仍然显示为add使用inspect.stack()[1].function获取调用方函数名方便快速定位“谁调用了当前函数”使用try...except...finally保证无论函数成功还是异常记录都会保存使用records列表保存全量调用记录方便后续批量导出或生成报告。在 Wrapture 的设计体系中这类逻辑通常会被抽象为“装饰器 观察者”或者“装饰器 钩子”的模式。你在项目里使用 Wrapture 时只需要专注业务逻辑例如“记录哪些函数”“记录完是否推送告警”“超过多少毫秒需要标记为慢调用”。4.5 从标准装饰器到 Wrapture 思路的演进如果你在阅读 Wrapture 文档时发现它提供了类似wrap的入口那么上面代码可以进一步收敛为import wrapture wrapture.wrap def add(a, b): return a b这种写法的好处是默认就拥有了函数元信息保留、参数校验扩展点、返回值观察点、异常透传点等能力而不需要每个项目重复维护一套CallTracker工具类。具体装饰器名称请以你安装版本的文档为准。5. 实战案例测试替换与行为模拟5.1 场景设定假设你的项目里有一个payment.py模块其中charge函数负责调用真实的第三方支付接口。在本地开发和测试时你不想真的扣款但你又希望验证“调用支付接口后订单状态是否更新正确”这段业务逻辑。这种场景就是最典型的“测试替换”。5.2 项目文件结构新建两个文件wrapture-demo/ ├── payment.py └── test_order.py5.3 被测业务代码payment.py内容如下# 文件路径wrapture-demo/payment.py import time def charge(user_id: int, amount: float) - dict: 真实支付接口本文示例中假装它会联网。 # 注意真实环境请不要在测试阶段调用这个函数 time.sleep(0.5) return { user_id: user_id, amount: amount, status: success, transaction_id: TX-2024-000001, } def create_order(user_id: int, amount: float) - dict: 创建订单并调用支付。 result charge(user_id, amount) if result[status] success: return {order_id: 1001, pay_status: paid, transaction_id: result[transaction_id]} return {order_id: 1002, pay_status: failed}业务逻辑很简单create_order内部调用charge根据支付结果返回订单状态。5.4 使用 unittest.mock 实现替换test_order.py内容如下# 文件路径wrapture-demo/test_order.py from unittest.mock import patch from payment import create_order def test_create_order_success(): with patch(payment.charge) as mock_charge: # 配置 mock 返回值 mock_charge.return_value { user_id: 1, amount: 99.9, status: success, transaction_id: TX-MOCK-001, } order create_order(1, 99.9) # 断言订单支付成功 assert order[pay_status] paid assert order[transaction_id] TX-MOCK-001 # 断言 charge 被真实调用了一次 mock_charge.assert_called_once_with(1, 99.9) print(测试通过mock 替换条件下订单状态正确) def test_create_order_failed(): with patch(payment.charge) as mock_charge: mock_charge.return_value { user_id: 1, amount: 10.0, status: failed, transaction_id: , } order create_order(1, 10.0) assert order[pay_status] failed print(测试通过支付失败条件下订单状态正确) if __name__ __main__: test_create_order_success() test_create_order_failed()在项目目录下执行python test_order.py如果代码运行无误输出大致如下测试通过mock 替换条件下订单状态正确 测试通过支付失败条件下订单状态正确注意这里的patch(payment.charge)替换的是payment模块里的全局名字charge而不是原始函数对象本身。这是 mock 替换非常重要的一个知识点——替换的位置是模块的命名空间。如果在test_order.py中使用from payment import create_ordercreate_order函数内部查找charge时仍然会在payment模块的全局命名空间中查找因此patch(payment.charge)可以生效。5.5 把替换能力封装成更友好的 API在实际工程中我们经常不想直接散落大量patch调用。可以做一个简单的上下文管理器# 文件路径wrapture-demo/replacer.py from contextlib import contextmanager from unittest.mock import patch contextmanager def mock_function(target, return_valueNone, side_effectNone): 替换目标函数并在退出上下文后自动恢复。 patcher patch(target, return_valuereturn_value, side_effectside_effect) mock_obj patcher.start() try: yield mock_obj finally: patcher.stop()用法如下from replacer import mock_function from payment import create_order def test_paid_order(): with mock_function(payment.charge, return_value{ status: success, transaction_id: TX-MOCK-002, }): order create_order(1, 50) assert order[pay_status] paid if __name__ __main__: test_paid_order() print(封装替换函数运行成功)这就是 Wrapture 类库在“测试替换”上希望提供的基础能力让替换函数的方式尽量简洁、可复用、安全并且不会在测试结束后污染环境。5.6 Wrapture 与 mock 的边界如果 Wrapture 已经提供了追踪和替换能力它和unittest.mock是什么关系可以这样理解unittest.mock是 Python 标准库的通用 mock 工具Wrapture 是更偏向“统一包装基础能力”的库在测试替换场景中两者可以配合使用也可以用 Wrapture 提供的高级包装器来简化追踪、监控、替换的代码量。如果你是项目负责人最理想的做法是用 Wrapture 统一封装函数追踪与可替换扩展点在测试代码中结合标准库 mock 对边界接口做打桩两者各司其职。6. 常见问题与排查思路6.1 安装了 Wrapture 却导入失败问题现象常见原因解决思路ModuleNotFoundError: No module named wrapture没有安装成功使用 python -m pip install wrapture 重新安装安装成功但导入失败当前 Python 环境和安装环境不一致在虚拟环境中重新安装版本冲突依赖库版本与当前环境不兼容查看官方文档要求的 Python 版本排查步骤先执行pip show wrapture确认包是否已经安装执行python -c import sys; print(sys.executable)确认当前解释器路径如果用的是 IDE检查 IDE 解释器设置如果首次安装建议新建一个干净的虚拟环境测试。6.2 追踪器导致递归调用或死循环在装饰器内部调用原函数时如果误用了原函数名可能会造成递归调用。示例def my_decorator(func): def wrapper(*args, **kwargs): result func(*args, **kwargs) # 正确调用传入的原函数 return result return wrapper def my_decorator_error(func): def wrapper(*args, **kwargs): result my_decorator_error(*args, **kwargs) # 错误递归调用装饰器自己 return result return wrapper排查思路检查装饰器内部是否调用了传入参数func如果看到RecursionError优先检查闭包内是否引用了外层同名函数标记函数是否被重复包装例如同一个函数被同一个装饰器包装两次。6.3 包装后函数名变化影响业务判断如果包装时没有使用functools.wraps那么函数元信息会丢失。有些框架会依赖函数名进行路由或权限判断这样就会导致问题。解决方案在自定义装饰器中使用functools.wraps(func)如果使用 Wrapture 类库优先使用其内置包装器通过func.__wrapped__访问最原始的函数是 Python 中常用的“穿透包装”手段。6.4 mock 替换没有生效最常见的原因是 patch 的目标写错。例如模块 A 内部使用from B import func导入了函数后面的代码直接指向 A.func此时如果 patchB.func不会生效。要 patchA.func。排查步骤先确认被测函数是在哪个模块文件里调用目标函数找到目标函数在调用模块中的访问名将 patch 参数替换为“调用模块名.函数名”。6.5 测试替换后没有恢复原函数有些新手会在测试中直接给函数赋值例如payment.charge fake_charge如果后面没有恢复就会污染后续测试。正确做法是尽量使用patch或mock_function这样的上下文管理器保证退出上下文时自动恢复。如果必须手动赋值建议写在try...finally中original_charge payment.charge try: payment.charge fake_charge # 测试逻辑 finally: payment.charge original_charge6.6 追踪信息太多影响性能在高频调用函数上全量打印日志会严重拖慢系统。解决方案开启采样追踪例如只记录超过 N 毫秒的调用将追踪信息写入独立日志文件而不是和控制台日志混在一起使用异步队列异步落库在测试环境开启详细追踪在生产环境关闭参数记录只保留耗时和异常。# 示例只记录慢调用 SLOW_THRESHOLD_MS 100 def slow_call_tracker(func): functools.wraps(func) def wrapper(*args, **kwargs): start time.perf_counter() result func(*args, **kwargs) cost_ms (time.perf_counter() - start) * 1000 if cost_ms SLOW_THRESHOLD_MS: print(f[SLOW] {func.__qualname__} 耗时 {cost_ms:.2f} ms) return result return wrapper7. 最佳实践与工程建议7.1 区分“调试追踪”和“永久埋点”函数追踪有两个完全不同的目标临时调试马上定位问题定位完就删除代码永久埋点作为监控和日志体系的一部分长期存在。这两者的代码要求和要求完全不同。临时调试可以随意打印到标准输出永久埋点则必须考虑日志框架、日志级别、敏感信息脱敏、数据采样等问题。建议在项目中用环境变量或配置项控制追踪开关import os TRACE_ENABLED os.getenv(TRACE_ENABLED, false).lower() true只有当环境变量开启时追踪器才真正打印信息避免因为某次调试代码误提交导致线上日志爆炸。7.2 多装饰器场景遵从“洋葱模型”当一个类函数上叠加了多个装饰器时执行顺序是从下往上靠近函数体的先执行外层后执行。例如decorator_a decorator_b def func(): pass实际调用顺序是先经过decorator_a的外层再进入decorator_b的外层最后执行原函数。返回时顺序相反。这块设计时要注意职责单一的装饰器更利于组合每个装饰器最好只做一件事不要在装饰器里吞掉异常除非你有明确的兜底策略。7.3 测试替换时遵循最小权限“测试替换”能力虽然好用但也有滥用风险。例如在测试中大面积 mock 掉被测函数内部依赖可能导致测试通过但业务实际已经坏了。推荐原则只替换外部不稳定依赖比如支付接口、短信接口、邮件服务尽量少 mock 项目内部函数多依赖真实行为在 mock 返回值时数据结构要和真实接口保持一致不要为了“跑通测试”而不顾契约一致性。7.4 敏感信息与日志边界不管是函数追踪还是测试替换都可能接触到函数入参、返回值。如果这些数据中包含手机号、身份证、密钥、Token 等敏感信息直接输出到日志会带来安全风险。处理思路黑白名单对敏感参数名如 password、token、secret做脱敏截断长文本只打印前 N 个字符配置化输出默认只记录函数名、耗时、状态不记录参数内容日志分级参数详情只输出到 DEBUG 级别。简单脱敏示例SENSITIVE_KEYWORDS (password, token, secret, api_key) def safe_repr(value: Any) - str: text repr(value) for keyword in SENSITIVE_KEYWORDS: if keyword in text.lower(): return redacted return text[:200]7.5 理解__wrapped__链Python 标准库中functools.wraps会把原始函数记录到__wrapped__属性中。这套机制很重要因为很多框架会通过它穿透装饰器拿到最原始的函数定义从而得到正确的签名和文档。如果你自己实现装饰器请务必这样做def my_decorator(func): functools.wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper这样my_decorator包装后的函数仍然能获取原函数信息。如果你的代码里有统一排查函数性能的需求用__wrapped__也能判断一个函数是否已经被包装过。7.6 Wrapture 在生产中的引入建议如果要在正式项目中引入 Wrapture 作为统一包装层建议先小范围试点而不是一次替换全项目已有装饰器。具体路径在非核心模块试用观察调用追踪和函数替换是否符合预期验证包装后函数的签名、文档、类型提示是否保持一致评估性能损耗特别是高频调用场景将包装层抽象为公共模块避免在业务代码里直接散落大量装饰器编写配套的单元测试覆盖“包装前”“包装后”行为一致性。这种渐进式引入方式可以降低风险也能让团队逐步熟悉库的能力边界。8. 总结与下一步学习方向本文围绕 Python 库 Wrapture 的核心能力——函数追踪与测试替换从 Python 装饰器基础原理出发讲解了函数包装的常见痛点、追踪器的设计思路、测试替换的实现方式并给出了完整的可运行示例与常见排查办法。项目中可以带走的知识点包括Wrapture 的核心价值是把“函数包装”这件事做得更安全、更统一函数追踪的本质是在调用前、调用后、异常时插入观察逻辑测试替换的关键是修改模块命名空间的指向而不是修改原函数使用functools.wraps和patch是最基本的两种能力引入任何包装库前都要先确认它对函数元信息、类方法、签名保留是否友好。下一步你可以继续学习这几个方向深入阅读 Wrapture 的官方文档与更新日志关注其最新 API学习inspect模块的更多用法理解签名与参数绑定研究wrapt库的设计因为它在 Python 包装器中影响深远结合 pytest 框架在项目里搭建一套基于 mock 和函数追踪的测试基础设施分析装饰器模式在 Flask、Django 等 Web 框架中的应用例如登录校验、权限控制、缓存装饰器。如果本文对你有帮助可以收藏备用。也建议你动手写一个小工具用一个装饰器追踪项目里所有耗时超过 200 毫秒的调用输出结果到一个独立文件中。这个练习能帮你快速掌握函数包装、追踪、拆解问题的全部思路。
返回列表