猴子补丁能改requests.get,但必须在目标被导入和调用前替换模块级函数;需用functools.wraps保留原函数元信息;测试应优先用unittest.mock.patch自动管理生命周期。

什么是猴子补丁,它真能改第三方库的 requests.get 吗?
能,但得看改的是不是实例方法、类方法还是模块级函数,且必须在目标代码被导入和调用前完成替换。猴子补丁本质是直接给对象(模块、类、实例)的属性赋新值,Python 的动态性允许这么做,但它不修改源码,只影响当前解释器运行时的行为。
常见误判点:import requests 后立刻 patch requests.get 是有效的;但如果已有其他模块先导入了 requests 并缓存了旧引用(比如通过 from requests import get),那 patch 就失效了——因为那个 get 是独立绑定的函数名,不再指向 requests.get。
- 优先 patch 模块级函数(如
requests.get、json.loads),最稳定 - 避免 patch 类的实例方法(如
requests.Session.request)后又创建新实例——老实例不受影响,新实例才走新逻辑 - patch 必须早于任何依赖该行为的代码执行,推荐放在
main()开头或测试 setup 阶段
怎么安全地 patch 一个函数并保留原功能?
直接赋值会丢失原函数的签名、文档和装饰器元信息,导致 IDE 提示异常、help() 失效,甚至某些框架(如 FastAPI)路由注册失败。正确做法是用 functools.wraps 包装新函数。
import requests
from functools import wraps
original_get = requests.get
@wraps(original_get)
def patched_get(*args, **kwargs):
print(f"[DEBUG] requests.get called with {args}")
return original_get(*args, **kwargs)
requests.get = patched_get
注意:不能写成 requests.get = lambda *a, **k: ...,那样会丢掉 __name__ 和 __doc__,很多工具链依赖这些属性。
立即学习“Python免费学习笔记(深入)”;
- 务必先保存原函数引用,再赋新值,否则 patch 后无法调用原逻辑
- 如果原函数带类型注解(如
def get(url: str, ...) -> Response:),@wraps会一并保留 - 不要在 patch 函数里做耗时操作(如日志写磁盘),否则拖慢所有
requests.get调用
patch 类方法时为什么新创建的实例不生效?
因为类方法绑定发生在实例化时。如果你 patch 的是 requests.Session.request,那么 patch 前已创建的 Session 实例,其内部仍持有对原方法的引用;只有 patch 后新建的实例,才会通过类查找机制拿到新方法。
更隐蔽的问题:有些库(如 aiohttp)会在类定义时把方法绑定到实例属性(self._request = self._request_impl),这种情况下 patch 类方法完全无效,必须 patch 实例自身的属性,或者改构造逻辑。
- 验证是否生效:打印
session.request.__func__ is 新函数,而不是只看session.request的__name__ - 若需影响已有实例,得遍历所有活跃实例手动重绑(不推荐,易漏且难维护)
- 真正需要改行为时,优先考虑子类继承 + 重写,比 patch 更清晰可控
测试中用 unittest.mock.patch 还是手动赋值?
测试场景下,无条件选 unittest.mock.patch。它自动处理 patch/restore 生命周期,避免测试间污染;支持装饰器、上下文管理器、类级 patch 等多种作用域控制;还能断言调用次数、参数等。
from unittest.mock import patch
import requests
def test_api_call():
with patch("requests.get") as mock_get:
mock_get.return_value.status_code = 200
result = fetch_user(123)
assert result["id"] == 123
mock_get.assert_called_once_with("https://api.example.com/users/123")
手动赋值适合调试、临时绕过限制或非测试环境(如开发期加调试日志),但必须自己确保恢复,否则可能影响后续逻辑。
-
patch("requests.get")中的字符串路径必须是目标对象**被使用的位置**,不是定义位置(即 "requests" 模块名,不是 "requests.api.get") - mock 对象默认返回
MagicMock,若需返回真实对象(如Response实例),要用return_value=real_obj - 全局 patch(如
@patch("requests.post"))要小心,容易让多个测试相互干扰


















