什么是 “猴子补丁”(monkey patching)?
支付接口忽然开始拒单,报错就一行:
invalid amount format: 19.9
这地方我第一眼不会去怀疑网络,也不会先翻一堆配置。金额字段从 19.90 变成 19.9,八成是某个 SDK 在拼请求参数时偷懒了。
麻烦的是,这个 SDK 是第三方包,线上版本不能马上升级,源码也不想直接改。这个时候,Python 里就有人会掏出一个东西:猴子补丁,monkey patching。
所谓猴子补丁,就是在程序运行时,直接替换已有模块、类、对象里的属性或方法。
不是继承。
不是改源码。
也不是重新发一个包。
就是运行到这里,把它换掉。
比如第三方 SDK 里大概是这么写的:
# pay_sdk.py
classPayClient:
defbuild_payload(self, order):
return {
"order_no": order["order_no"],
"amount": str(order["amount"]),
"channel": order["channel"]
}
业务里传进去的是浮点数:
order = {
"order_no": "A20260118001",
"amount": 19.9,
"channel": "wx"
}
它一转字符串,就成了 19.9。对方支付网关偏要两位小数,直接拒。
正常做法当然是让 SDK 修,或者我们调用前把数据整理好。但线上已经卡住了,回滚也不划算,这时候可以先打一层补丁:
# patch_pay_sdk.py
from decimal import Decimal, ROUND_HALF_UP
from pay_sdk import PayClient
deffixed_build_payload(self, order):
amount = Decimal(str(order["amount"])).quantize(
Decimal("0.01"),
rounding=ROUND_HALF_UP
)
return {
"order_no": order["order_no"],
"amount": format(amount, ".2f"),
"channel": order["channel"]
}
PayClient.build_payload = fixed_build_payload
只要这段代码在 PayClient 被真正使用前执行,后面所有 PayClient 实例调用的就是新方法。
import patch_pay_sdk
from pay_sdk import PayClient
client = PayClient()
payload = client.build_payload({
"order_no": "A20260118001",
"amount": 19.9,
"channel": "wx"
})
print(payload)
输出:
{'order_no': 'A20260118001', 'amount': '19.90', 'channel': 'wx'}
这就是猴子补丁。
它不神秘,甚至有点粗暴。Python 允许你在运行时改类方法,因为类本身也是对象,方法也只是挂在类上的一个属性。你把这个属性重新赋值,它就换了。
我不太喜欢把猴子补丁讲得很优雅。它本来就不是优雅方案。
它更像线上抢修时拿的一卷胶带。
能救急,但别拿它装修房子。
猴子补丁常见的几个位置:
测试里临时替换外部依赖。
比如不想真的调短信服务:
classSmsGateway:
defsend(self, mobile, text):
raise RuntimeError("real sms api should not run in test")
deffake_send(self, mobile, text):
print(f"[test-sms] {mobile}: {text}")
return {"ok": True, "msg_id": "fake-001"}
SmsGateway.send = fake_send
gateway = SmsGateway()
print(gateway.send("13800000000", "验证码 246810"))
还有一种是补第三方库的小坑。
比如某个库没有打日志,排查问题时看不到入参。我一般不会上来就全局改,先包一层,打印关键字段:
from pay_sdk import PayClient
_old_build_payload = PayClient.build_payload
deftraced_build_payload(self, order):
print(
"[pay-sdk-trace]",
"order_no=", order.get("order_no"),
"amount=", order.get("amount")
)
return _old_build_payload(self, order)
PayClient.build_payload = traced_build_payload
这个写法比直接覆盖稍微稳一点,原逻辑还在,只是在前面插了一段 trace。
但这里有个坑,补丁顺序一定要盯住。
如果某个模块在补丁执行前就把方法缓存走了,你后面再改类方法,未必影响它。
比如这种代码就有味道:
from pay_sdk import PayClient
client = PayClient()
build_payload_func = client.build_payload
后面你再执行:
PayClient.build_payload = fixed_build_payload
client.build_payload 会变,但前面那个 build_payload_func 可能还握着旧方法。排查这类问题时,我一般先搜两样东西:from xxx import 和函数赋值缓存。猴子补丁最烦的不是不会写,是你不知道它到底有没有生效。
再说一个更容易翻车的地方:作用范围。
你以为只是改了一个地方,实际上整个进程都变了。
PayClient.build_payload = fixed_build_payload
这不是只影响某个订单,也不是只影响某个请求。只要在这个 Python 进程里,所有 PayClient 实例都会走新逻辑。
所以猴子补丁最好做到几件事:
补丁文件单独放,别散在业务代码里。
启动时集中加载,别在请求处理中偷偷 patch。
原方法尽量保留,出问题能回退。
补丁旁边写清楚原因,不要只写一句“fix bug”。
像这样我还能接受:
# patches/pay_amount_format.py
# 原因:pay-sdk 2.1.7 金额用 str(float) 生成,网关要求两位小数
# 删除条件:升级到 pay-sdk >= 2.1.9 后删除
from decimal import Decimal, ROUND_HALF_UP
from pay_sdk import PayClient
_raw_build_payload = PayClient.build_payload
defbuild_payload_with_amount_fix(self, order):
payload = _raw_build_payload(self, order)
if"amount"in payload:
amount = Decimal(str(payload["amount"])).quantize(
Decimal("0.01"),
rounding=ROUND_HALF_UP
)
payload["amount"] = format(amount, ".2f")
return payload
PayClient.build_payload = build_payload_with_amount_fix
主程序入口里显式加载:
# app.py
import patches.pay_amount_format
from pay_sdk import PayClient
别嫌这个 import 难看。它就该难看一点。猴子补丁如果藏得太深,半年后排查问题的人会骂人。
猴子补丁可以用,但我一般只在这几种情况下用:
第三方库有小 bug,等不到发版。
测试环境需要替换外部服务。
线上临时止血,后面必须补正式方案。
运行时加一点 trace,排查完就删。
不适合拿它做长期架构。
尤其别用猴子补丁到处改标准库、改框架默认行为、改一堆同事都不知道的全局方法。那种代码跑起来像没问题,真出事的时候,日志、源码、调用链三边对不上,很难查。