彻底弄懂Python中的 Moneky Patch

在使用 Python 的时候,我们可能会经常听到一个名词 —— Moneky Patch,尤其是在搜索 Python 协程相关的信息的时候。那么什么是 Moneky Patch 呢?它和 Python 的协程有什么关系呢?它和 eventlet 有什么关系呢?最重要的,它和猴子有什么关系呢?ᶘ ͡°ᴥ͡°ᶅ

最近在研究 OpenStack 的 loopingcall,它主要就是用 eventlet 启动协程实现并发执行多个函数的,为了后面写相关的博客,本着刨根问底的原则,先从 Moneky Patch 开始吧。

在这里插入图片描述

猴子补丁?

什么是 Moneky Patch

首先 Moneky Patch 不是一个官方库,也不是一个第三方库,它说的是一种用法,可以说是一种 Magic 方法(更多 Python 黑魔法可以参考 Python 黑魔法手册)。当然,你去PyPi官网去搜monkeypatch,是可以搜到很多库,它们都是具体场景下的 Moneky Patch 的具体实现。

利用动态语言的特点,在运行时替换指定函数的代码,达到实时更改函数行为的效果,这就叫做“打Moneky Patch”。Python 中实现Moneky Patch ,主要是利用 Python 的 import 机制和“一切皆对象”的特点。

和 Python 的协程的关系

在 Python 3 提供 async/await关键字用来支持协程之前,有很多第三方库都是利用yield表达式来实现协程的,例如eventletgreenlet

所谓协程,就是由程序员自己控制代码的上下文切换,在一个协程的代码执行到 IO 阻塞的地方时,将 CPU 切换到其他不需要 IO 的协程上去,这样既实现了并发又提高了效率。

而一些涉及到 IO 的 Python 库,比如 socket 这种,已经封装好了很多底层接口,要从这些接口中增加控制上下文切换的代码,就要重写很多 Python 标准库。以前的那些库已经被大量使用了,人们也比较熟悉,搞几个新的库来替代显然不合适,于是greenlet就采用打Moneky Patch的方式来动态替换这些 IO 相关的库里的方法来实现协程。

和猴子的关系

至于为什么叫猴子补丁,我感觉是因为猴子给人的印象是比较爱捣乱,而打了Moneky Patch之后,源码在运行时才会被动态替换。你如果没看到哪里打了Moneky Patch,看到的源码和实际运行时的情况就不同了,整个代码结构也会不可避免地被打乱,这就像一只在你的代码里捣乱的猴子一样,好像还挺形象的吧。

这让我想起来前几年很火的各大视频网站的破解会员的浏览器插件,也都是叫“油猴脚本”、“暴力猴”这种带猴子的名字,其实这种会员破解脚本的破解原理就是在javascript脚本里打Moneky Patch,在浏览器加载网页的javascript脚本时,动态替换某个会员认证的函数,跳过会员认证的逻辑。现在这种方式早已被网站们做了防护。

使用示例

上面说了很多,可能比较抽象,其实用一个例子来看就很容易理解,看完示例代码。再去看上面的解释就好理解多了。

定义一个Dog类,有一个speak方法。

# Python3
class Dog:
    def speak(self):
        print('wang wang wang')


Dog.speak()

运行结果:

wang wang wang

后来我们想让Dog说中国话,又不想改原来的代码,就在外面重新定义了一个说话方法,然后把Dog.speak直接替换成新方法。

# Python3
class Dog:
    def speak(self):
        print('wang wang wang')


def 说话():
    print('汪 汪 汪')


Dog.speak = 说话
Dog.speak()

运行结果:

汪 汪 汪

由于 Python 中一切皆对象,这个替换过程就像把一个变量赋值两次,第一次的值直接被覆盖掉了。

# a的值最终是2
a = 1
a = 2

上面这个例子可能看上去有点奇怪,因为大多数情况我们是不需要使用 Moneky Patch 的。之所以用这种方式主要是在于两点:一个是不想改原来的代码,一个是不改变原来代码的调用方式。

需要使用 Moneky Patch 的少数场景有哪些呢?

使用场景

首先要明确一点,使用Moneky Patch会打乱原有的代码结构,而且代码看上去会很奇怪,所以应该坚持能不使用Moneky Patch就不使用的原则。如果整个项目都是自己的代码,那我们也大可不必使用 Moneky Patch,直接修改源码或者封装多态或者搞个子类继承,都比上面这种方式更清晰更优雅。

但是,如果想要改变第三方包的某个行为,又或者是想要改变公共模块的某个行为,这时候就不得不使用 Moneky Patch了。

1. 最常见的使用场景就是在单元测试中使用的mock库。

为什么我们可以在测试用例中随意伪造业务代码的返回值呢?就是因为mock单元测试在运行的时候,动态地把业务代码里的函数给替换成了其他函数,源代码的调用方式没有改变,返回结果却成了我们设定的 fake 值。

2. 某个第三方库硬编码了某些东西,我们无法通过接口传参来实现特定的效果。

由于 Python 库的代码和业务代码一般不在同个工程下,而且 Python 库是公共使用的,我们不能因为自己的特殊需要就去更改第三方库的源码。

例如,有一个第三方库third_party_module,自己的代码my_code.py

# third_party_module.py
def step1():
    print('step1')


def step2():
    print('step2')


def run():
    step1()
    step2()
# my_code.py
import third_party_module

third_party_module.run()

运行结果:

step1

step2

我们想在step1step1中间加一个自己的步骤step3,就可以像下面这样:

# my_code.py
import third_party_module


def step3():
    print('step3')


# 函数名可以随意定义,dog_patch、cat_patch,不带patch都可以
def monkey_patch():
    def my_run():
        third_party_module.step1()
        step3()
        third_party_module.step2()

    third_party_module.run = my_run


monkey_patch()
third_party_module.run()

运行结果:

step1

step3

step2

3. 想要改变公共代码模块的某个行为实现自己的特殊用法。

公共代码模块虽然不像第三方库那样不能修改,但是如果许多不同的业务组都在使用同一个公共模块,而我们的需求只是自己的特殊场景使用,也是不宜直接修改源码的,这时候就可以打Moneky Patch来解决,使用方法类似上面第三方库的场景。

更好的使用示例

如果我们不想让替换的代码全局生效,其实可以参考mock库的上下文管理器的用法,来实现局部生效的效果。

结合上面的例子:

# my_code.py
import mock
import third_party_module


def step3():
    print('step3')


def my_run():
    third_party_module.step1()
    step3()
    third_party_module.step2()


print('invoke1 ...')
with mock.patch.object(third_party_module, 'run', my_run):
    third_party_module.run()

print('invoke2 ...')
third_party_module.run()

运行结果:

invoke1 …

step1

step3

step2

invoke2 …

step1

step2

最后再说一句,使用Moneky Patch会打乱原有的代码结构,破坏代码的可读性,在实际开发中我们应该尽量不使用Moneky Patch

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值