详解Python编程中对Monkey Patch猴子补丁开发方式的运用


Posted in Python onMay 27, 2016

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 相关文章推荐
浅谈Python单向链表的实现
Dec 24 Python
使用python进行波形及频谱绘制的方法
Jun 17 Python
Python为何不能用可变对象作为默认参数的值
Jul 01 Python
Python模块汇总(常用第三方库)
Oct 07 Python
Python中实现输入超时及如何通过变量获取变量名
Jan 18 Python
Python3搭建http服务器的实现代码
Feb 11 Python
python re模块匹配贪婪和非贪婪模式详解
Feb 11 Python
Python数组并集交集补集代码实例
Feb 18 Python
django model 条件过滤 queryset.filter(**condtions)用法详解
May 20 Python
python接口自动化之ConfigParser配置文件的使用详解
Aug 03 Python
解决Django transaction进行事务管理踩过的坑
Apr 24 Python
opencv用VS2013调试时用Image Watch插件查看图片
Jul 26 Python
Python程序中的观察者模式结构编写示例
May 27 #Python
Windows下python2.7.8安装图文教程
May 26 #Python
Java Web开发过程中登陆模块的验证码的实现方式总结
May 25 #Python
剖析Python的Twisted框架的核心特性
May 25 #Python
实例解析Python的Twisted框架中Deferred对象的用法
May 25 #Python
详解Python的Twisted框架中reactor事件管理器的用法
May 25 #Python
使用Python的Twisted框架编写非阻塞程序的代码示例
May 25 #Python
You might like
php验证是否是md5编码的简单代码
2014/04/01 PHP
11个PHPer必须要了解的编程规范
2014/09/22 PHP
php更新mysql后获取改变行数的方法
2014/12/25 PHP
微信公众号OAuth2.0网页授权问题浅析
2017/01/21 PHP
如何在Mozilla Gecko 用Javascript加载XSL
2007/01/09 Javascript
漂亮的widgets,支持换肤和后期开发新皮肤
2007/04/23 Javascript
JQuery textlimit 显示用户输入的字符数 限制用户输入的字符数
2009/05/14 Javascript
Javascript解决常见浏览器兼容问题的12种方法
2010/01/04 Javascript
js和html5实现手机端刮刮卡抽奖效果完美兼容android/IOS
2013/11/18 Javascript
javascript教程:关于if简写语句优化的方法
2014/05/17 Javascript
JavaScript DOM节点添加示例
2014/07/16 Javascript
jQuery实现在最后一个元素之前插入新元素的方法
2015/07/18 Javascript
js判断所有表单项不为空则提交表单的实现方法
2016/09/09 Javascript
jquery easyui validatebox remote的使用详解
2016/11/09 Javascript
JavaScript中for循环的几种写法与效率总结
2017/02/03 Javascript
html+javascript+bootstrap实现层级多选框全层全选和多选功能
2017/03/09 Javascript
详解VUE中v-bind的基本用法
2017/07/13 Javascript
使用vue实现grid-layout功能实例代码
2018/01/05 Javascript
Vue项目全局配置微信分享思路详解
2018/05/04 Javascript
浅谈Webpack核心模块tapable解析
2018/09/11 Javascript
在SSM框架下用laypage和ajax实现分页和数据交互的方法
2019/09/27 Javascript
如何利用nodejs自动定时发送邮件提醒(超实用)
2020/12/01 NodeJs
[38:32]完美世界DOTA2联赛循环赛 Forest vs DM 第二场 11.06
2020/11/06 DOTA
Python使用xlrd读取Excel格式文件的方法
2015/03/10 Python
Python常见异常分类与处理方法
2017/06/04 Python
Python基于pyCUDA实现GPU加速并行计算功能入门教程
2018/06/19 Python
python Pandas库基础分析之时间序列的处理详解
2019/07/13 Python
美国标志性加大尺码时装品牌:Ashley Stewart
2016/12/15 全球购物
台湾线上百货零售购物平台:friDay购物
2017/08/18 全球购物
优秀员工自荐信范文
2013/10/05 职场文书
学生期末评语大全
2014/04/30 职场文书
红领巾广播站广播稿(3篇)
2014/09/20 职场文书
村党支部群众路线教育实践活动对照检查材料
2014/09/26 职场文书
2014年业务工作总结
2014/11/17 职场文书
python制作图形界面的2048游戏, 基于tkinter
2021/04/06 Python
mysql脏页是什么
2021/07/26 MySQL