Python技术迷

什么是 “猴子补丁”(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,排查完就删。

不适合拿它做长期架构。

尤其别用猴子补丁到处改标准库、改框架默认行为、改一堆同事都不知道的全局方法。那种代码跑起来像没问题,真出事的时候,日志、源码、调用链三边对不上,很难查。