视频1 视频21 视频41 视频61 视频文章1 视频文章21 视频文章41 视频文章61 推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37 推荐39 推荐41 推荐43 推荐45 推荐47 推荐49 关键词1 关键词101 关键词201 关键词301 关键词401 关键词501 关键词601 关键词701 关键词801 关键词901 关键词1001 关键词1101 关键词1201 关键词1301 关键词1401 关键词1501 关键词1601 关键词1701 关键词1801 关键词1901 视频扩展1 视频扩展6 视频扩展11 视频扩展16 文章1 文章201 文章401 文章601 文章801 文章1001 资讯1 资讯501 资讯1001 资讯1501 标签1 标签501 标签1001 关键词1 关键词501 关键词1001 关键词1501 专题2001
Python编程中对MonkeyPatch猴子补丁开发方式
2020-11-27 14:26:16 责编:小采
文档


Monkey Patch猴子补丁方式是指在不修改程序原本代码的前提下,通过添加类或模块等方式在程序运行过程中加入代码,下面就来进一步详解Python编程中对Monkey Patch猴子补丁开发方式的运用

Monkey patch就是在运行时对已有的代码进行修改,达到hot patch的目的。Eventlet中大量使用了该技巧,以替换标准库中的组件,比如socket。首先来看一下最简单的monkey patch的实现。

class Foo(object):
 def bar(self):
 print 'Foo.bar'

def bar(self):
 print 'Modified bar'

Foo().bar()

Foo.bar = bar

Foo().bar()

由于Python中的名字空间是开放,通过dict来实现,所以很容易就可以达到patch的目的。

Python namespace

Python有几个namespace,分别是

  • locals

  • globals

  • builtin

  • 其中定义在函数内声明的变量属于locals,而模块内定义的函数属于globals。

    Python module Import & Name Lookup

    当我们import一个module时,python会做以下几件事情

  • 导入一个module

  • 将module对象加入到sys.modules,后续对该module的导入将直接从该dict中获得

  • 将module对象加入到globals dict中

  • 当我们引用一个模块时,将会从globals中查找。这里如果要替换掉一个标准模块,我们得做以下两件事情

    将我们自己的module加入到sys.modules中,替换掉原有的模块。如果被替换模块还没加载,那么我们得先对其进行加载,否则第一次加载时,还会加载标准模块。(这里有一个import hook可以用,不过这需要我们自己实现该hook,可能也可以使用该方法hook module import)
    如果被替换模块引用了其他模块,那么我们也需要进行替换,但是这里我们可以修改globals dict,将我们的module加入到globals以hook这些被引用的模块。
    Eventlet Patcher Implementation

    现在我们先来看一下eventlet中的Patcher的调用代码吧,这段代码对标准的ftplib做monkey patch,将eventlet的GreenSocket替换标准的socket。

    from eventlet import patcher
    
    # *NOTE: there might be some funny business with the "SOCKS" module
    # if it even still exists
    from eventlet.green import socket
    
    patcher.inject('ftplib', globals(), ('socket', socket))
    
    del patcher
    
    inject函数会将eventlet的socket模块注入标准的ftplib中,globals dict被传入以做适当的修改。
    
    让我们接着来看一下inject的实现。
    
    __exclude = set(('__builtins__', '__file__', '__name__'))
    
    def inject(module_name, new_globals, *additional_modules):
     """Base method for "injecting" greened modules into an imported module. It
     imports the module specified in *module_name*, arranging things so
     that the already-imported modules in *additional_modules* are used when
     *module_name* makes its imports.
    
     *new_globals* is either None or a globals dictionary that gets populated
     with the contents of the *module_name* module. This is useful when creating
     a "green" version of some other module.
    
     *additional_modules* should be a collection of two-element tuples, of the
     form (, ). If it's not specified, a default selection of
     name/module pairs is used, which should cover all use cases but may be
     slower because there are inevitably redundant or unnecessary imports.
     """
     if not additional_modules:
     # supply some defaults
     additional_modules = (
     _green_os_modules() +
     _green_select_modules() +
     _green_socket_modules() +
     _green_thread_modules() +
     _green_time_modules())
    
     ## Put the specified modules in sys.modules for the duration of the import
     saved = {}
     for name, mod in additional_modules:
     saved[name] = sys.modules.get(name, None)
     sys.modules[name] = mod
    
     ## Remove the old module from sys.modules and reimport it while
     ## the specified modules are in place
     old_module = sys.modules.pop(module_name, None)
     try:
     module = __import__(module_name, {}, {}, module_name.split('.')[:-1])
    
     if new_globals is not None:
     ## Update the given globals dictionary with everything from this new module
     for name in dir(module):
     if name not in __exclude:
     new_globals[name] = getattr(module, name)
    
     ## Keep a reference to the new module to prevent it from dying
     sys.modules['__patched_module_' + module_name] = module
     finally:
     ## Put the original module back
     if old_module is not None:
     sys.modules[module_name] = old_module
     elif module_name in sys.modules:
     del sys.modules[module_name]
    
     ## Put all the saved modules back
     for name, mod in additional_modules:
     if saved[name] is not None:
     sys.modules[name] = saved[name]
     else:
     del sys.modules[name]
    
     return module

    注释比较清楚的解释了代码的意图。代码还是比较容易理解的。这里有一个函数__import__,这个函数提供一个模块名(字符串),来加载一个模块。而我们import或者reload时提供的名字是对象。

    if new_globals is not None:
     ## Update the given globals dictionary with everything from this new module
     for name in dir(module):
     if name not in __exclude:
     new_globals[name] = getattr(module, name)

    这段代码的作用是将标准的ftplib中的对象加入到eventlet的ftplib模块中。因为我们在eventlet.ftplib中调用了inject,传入了globals,而inject中我们手动__import__了这个module,只得到了一个模块对象,所以模块中的对象不会被加入到globals中,需要手动添加。
    这里为什么不用from ftplib import *的缘故,应该是因为这样无法做到完全替换ftplib的目的。因为from … import *会根据__init__.py中的__all__列表来导入public symbol,而这样对于下划线开头的private symbol将不会导入,无法做到完全patch。

    更多Python编程中对Monkey Patch猴子补丁开发方式相关文章请关注PHP中文网!

    下载本文
    显示全文
    专题