python的猴子补丁monkey patch

monkey_patch,这个词多少年前就在python编程中了解过,但是一直没有系统的总结下,近日又再次遇到这个概念,故此记录一下。

 

在python中由于其内部的实现机制导致其具备较大的编程灵活性,也正是如此,monkey_patch在python中更加有应用场景。monkey_patch这个概念就是在python程序运行过程中对其原有的某些函数、类或模块进行替换,monkey_patch往往出现在程序启动最初的地方。比较形象的一个例子:https://blog.csdn.net/zzh514742165/article/details/128343540

可以说在原有项目不进行细节改动的情况下我们通过使用monkey_patch可以实现对原有项目中的功能函数替换的目的,这也是为啥叫patch的原因。
 
 

 

给出一个自己的toy code:

  

 

 

 

 

main.py

复制代码
"""
import sub

import monkey_patch
monkey_patch.mk()

sub.sub_fun()
"""


import monkey_patch
monkey_patch.mk()

from sub import sub_fun


sub_fun()
复制代码

 

 sub.py

def sub_fun():
    print("this is sub module")

 

monkey_patch.py

复制代码
import sub
from sub import sub_fun


def sub_fun2():
    print("this is monkey_patch module")


def mk():
    sub.sub_fun = sub_fun2
    sub.sub_fun.__name__ = "monkey_patch_sub_fun"


    sub_fun = sub_fun2
    sub_fun.__name__ = "monkey_patch_sub_fun"
复制代码

 

 

需要注意的是这个monkey_patch对于import xx_module的情况还是比较好处理的,比较不好处理的就是那种from xx_module import xx 

对于这种from xx_module import xx 的情况,我们就需要在monkey_patch中同样编写from xx_module import xx 并且在主文件启动时把这个monkey_patch放在最前面,否则就会失效。

from xx_module import xx 操作其实等价于import xx_module;xx=xx_module.xx,可以说之所以这样的情况monkey_patch不好处理是因为monkey_patch中的函数执行是不能修改其他模块中的变量命名空间的,比如main.py中的变量的命名空间为__main__,monkey_patch.py中变量的命名空间为monkey_patch,如果在main.py中执行from xx_module import xx 操作,那么xx的考虑命名空间的全名为__main__.xx,而import xx_module 这种情况则不论在任何子模块中调用该import都会有相同的命名空间变量xx_module.xx ,因此这种情况才可以在monkey_patch中容易的进行替换。

 

之所以把monkey_patch.mk() 操作放在main.py最初始的地方可以使from xx_module import xx 有效是因为monkey_patch.py中已经执行了from xx_module import xx 这个操作并对此进行了替换,这样其他模块中再次执行from xx_module import xx 就不会有具体的操作,因为已经xx_module下的xx变量已经存在在全局的命名空间下。

 

总的来说, import xx_module 不论在启动模块还是子模块中执行操作后便在命名空间中留下了记录,以后不论任何模块通过xx_module.xx的方式来进行赋值或修改都是直接对全局唯一的变量进行操作的,并且所有模块都可以共享这个全局命名空间下的这个xx_module.xx变量。但是对于from xx_module import xx 操作来说,只有在第一次执行的模块内是可以对这个xx变量进行赋值或修改后其他模块也可以通过from xx_module import xx 方式读取该值的,但是其他模块即使通过该方式读取该值也不能对其修改后使其在所有子模块内被共享,因为此时的xx的命名框架是当前执行模块内的,换句话说如果使用from xx_module import xx 方式的话,只有在其第一次执行的模块内对其进行的操作是可以传递到其他模块的,而其他模块(不是第一次执行from xx_module import xx 操作的模块)对其的修改只在自身模块的命名框架下是有效的。

 

 

from xx_module import xx 方式调用xx,那么在任何子模块中执行后其命名空间都为其所执行模块的名称,也就是说如果是xyz.py模块调用那么其变量名可以视作xyz.xx,这也就是为什么只有第一次执行from xx_module import xx 方式调用的模块才可以对其进行修改。

 

import xx_module方式调用xx_module.xx,那么在所有模块中带命名空间的变量全名为xx_module.xx 。

 

为了更加形象的说明这个问题,我们更改main.py中的内容如下:

Demo1: 

main.py

# Demo1
import sub

import monkey_patch
monkey_patch.mk()

sub.sub_fun()

 

 

  

 

 

Demo2: 

main.py

# Demo2
import monkey_patch
monkey_patch.mk()

import sub

sub.sub_fun()

 

 

 

 

 

Demo3: 

main.py

# Demo3
import monkey_patch
monkey_patch.mk()

from sub import sub_fun


sub_fun()

 

 

 

 

 

Demo4: 

main.py 

# Demo4
from sub import sub_fun

import monkey_patch
monkey_patch.mk()


sub_fun()

 

 

 

可以看到使用monkey_patch时,如果from xx_module import xx 方式调用xx,那么必须要让monkey_patch的执行在第一次进行from xx_module import xx 操作之前,因此我们也可以认为进行monkey_patch时最好是在程序启动文件main.py的最早的位置,这样就可以最大程度上保证monkey_patch的效果。

 

 

 

 

=========================================================

 

 

 

对于整个模块进行monkey_patch替换则需要修改sys.modules中的内容。

 

 

 

复制代码
import sys
sys.modules['sys']

import sub
sys.modules['os']=sys.modules['sub']

import os
os.sub_fun()
print(os)
复制代码

 

 

--------------------------------------------------

 

 

对module模块进行monkey_patch逻辑更加简单些,对于内置模块来说都已经加入到了sys.modules中,对于其他模块在import的时候都是调用__import__语句,然后再把信息加入到sys.modules中。每个模块在执行import操作是都会在sys.modules中选择是否有同名的key,如果已经存在则不执行import操作,但是对于from xx import xx 操作则会执行重命名操作。

 

 

更加完整的例子:

 

 

 

main.py

复制代码
from monkey_patch import monkey_patch



monkey_patch('os')
import os
# os = monkey_patch('os')


print(os)
os.sub_fun()

import sub_sub
复制代码

 

monkey_patch.py

复制代码
def monkey_patch(module_name):
    assert isinstance(module_name, str)
    import sys
    if module_name not in sys.modules:
        module = __import__(module_name)
    else:
        import sub
        sys.modules[module_name]=sys.modules['sub']
        module = sys.modules[module_name]
        del sys.modules['sub']
    return module
复制代码

 

sub.py

print("hello, sub module")

def sub_fun():
    print("this is sub module")

 

sub_sub.py

import os
os.sub_fun()

 

结果:

 

 

 

--------------------------------------------------

 

如果把main.py内容改为:

复制代码
from monkey_patch import monkey_patch



# monkey_patch('os')
import os
os = monkey_patch('os')


print(os)
os.sub_fun()

import sub_sub
复制代码

结果:

 

 

这里是因为monkey_patch的执行在import后面,这样命名空间下的os变量是不能被改变的,因此需要显示explicit方式进行替换。

 

 

 ====================================================

 

 

总的来说,monkey_patch是一种最小程度破坏代码整体性的前提下对代码内容进行替换的操作,属于黑魔法性质的操作,虽然功能很强大,但是对于软件的编码整体性造成了破坏,因此monkey_patch一般都是用于临时调试的情况下,作为成熟的最终产品还是要尽量避免使用monkey_patch的,毕竟这东西终究是patch,但是对某些软件产品其本身设计了一些对python的built-in的内置模块或方法进行替换,那么使用monkey_patch也是一种很好的选择,比如gevent,当然这种情况还是比较少的。

 

 

 

=====================================================

 

 

 

参考:

https://blog.csdn.net/zzh514742165/article/details/128343540

https://www.cnblogs.com/robert871126/p/10107127.html

https://www.cnblogs.com/XiGang-/articles/6589730.html

https://blog.51cto.com/iceyao/1695266

https://www.cnblogs.com/robert871126/p/10107258.html

https://blog.csdn.net/seizef/article/details/5732657

https://zhuanlan.zhihu.com/p/37679547

 

posted on   Angry_Panda  阅读(861)  评论(0编辑  收藏  举报

相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)

导航

< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

统计

点击右上角即可分享
微信分享提示