Python Monkey Patching:运行时改写规则的工程边界
Monkey patching 是 Python 社区里争议最持久的技术之一。反对它的人说它是”全局副作用的温床”;拥护它的人说它是”动态语言的正当权利”。两边说的都不错,但都没解决一个实际问题:你在什么情况下该用它,在什么情况下该克制。
这篇文章不讲道德,只讲工程判断。
Python 对象模型为什么允许这件事
Monkey patching 能工作,根本原因是 Python 的对象模型是基于字典的。类和实例的属性查找都走 __dict__,方法也不例外。
1 | |
这不是黑魔法,是语言规范。LLMClient.complete 只是 LLMClient.__dict__["complete"] 的一个绑定名称,重新赋值就是替换它。
模块也是同理。模块对象是 sys.modules 里的一个命名空间,import 之后可以直接修改它的属性:
1 | |
理解这个,才能理解 monkey patching 的边界在哪里——它操作的是运行时的命名空间,不是源码,所以影响范围取决于你修改的是谁的 __dict__。
有价值的用法
1. 测试隔离:截断真实的 LLM 调用
AI 应用的测试有一个特殊困难:LLM 调用有延迟、有费用、结果不确定。直接跑集成测试成本太高,完全 mock 又测不到真实逻辑。
Monkey patching 可以在不改动任何业务代码的前提下替换掉 SDK 调用:
1 | |
unittest.mock.patch 本质上就是 monkey patching 的标准化封装——它帮你管理替换和恢复的生命周期,避免状态泄漏到其他测试。
2. 注入可观测性:不侵入业务代码的 tracing
在 Agent 系统里,你经常需要记录工具调用的入参、出参、耗时,但又不想让 tracing 代码侵入工具实现本身。
Monkey patching 可以在运行时为任意函数注入 instrumentation:
1 | |
这比在每个工具函数里手写 span.start() / span.end() 要干净得多。修改集中在一处,工具函数本身保持纯粹。
3. 第三方 SDK 的行为修正
有时候第三方库的行为有 bug,或者你需要在它的某个方法里加特殊逻辑,但又等不到上游修复。
这是 monkey patching 最有争议、但也最实际的用法。判断是否该这样做的标准只有一个:是否有更短的路径。
如果可以 fork、可以提 PR 并在内部打补丁包、可以用 subclassing 覆盖,优先走那些路。Monkey patching 第三方库的代价是:升级版本时补丁可能静默失效,且不容易被发现。
会出问题的用法
在模块级别 patch 但不恢复。修改 sys.modules 里的对象会影响所有 import 该模块的代码,包括你没意识到的间接依赖。测试之间的状态污染、并发环境下的竞态,都是这样来的。
1 | |
替换 __init__ 或 __new__。构造函数的调用时机和参数复杂,替换之后行为很难预测,尤其是有继承的情况下。
在生产代码路径里做 patch。Monkey patching 放在测试代码里是工程实践,放在业务主路径里几乎都是架构债务的信号——通常意味着应该重新设计接口,而不是运行时打补丁。
实际工程里的决策框架
遇到”需要改变某个函数行为”的需求时,按这个顺序排查:
- 能改接口吗? 如果是自己的代码,把依赖显式化、用 DI 注入、写 subclass。这是最干净的解法。
- 能用
unittest.mock.patch吗? 如果是测试场景,用标准库,不要自己写 patch 逻辑。 - 是第三方库的 bug 且短期内没有修复路径? 可以临时 patch,但一定要写清楚注释:哪个版本有问题、上游 issue 链接、预期在什么版本移除。
- 其他情况,基本上都不该用 monkey patching。
Python 的动态性让你可以做很多事,但”可以做”不等于”应该做”。Monkey patching 是语言给你的一把手术刀——场景对了很有用,场景不对留下的是后遗症。