Monkey Patch(猴子补丁)

本节内容可以 大概了解 就好,之所以特意将 Monkey Patch 提出来说明,是因为,如果你使用 Python 真正的、独立的去完成一个项目、产品,它在某一刻就摆在那里,你必须要用到。
当然,浅尝辄止,或者使用 Python 只是完成一些简单的事情,这样做热补丁的方式,应该永远不会触碰到。

什么是猴子补丁

Bug 本意是 虫子,怎么就成了 Bug 呢?你搜索引擎上找找,很容易找到出处。
那么 Monkey Patch 是什么东西,为什么叫 Monkey 呢?
Monkey Patch 的翻译,或许叫 热补丁,或许叫 程序运行过程中的补丁,至于为什么叫 Monkey,出处似乎并不完全统一,也许就是猴子上蹦下跳的特性吧。

有人把 Monkey Patch 也形容为黑魔法,毕竟脱离常规的理解,确实看起来很像黑魔法。
但如果你已经理解了变量 的概念(可以再参考 《FirstWeb》),也明白了 package 的 import 逻辑,更清楚一个 module 也是一个变量的基本概念,那么 Moneky Patch 这种灵活应用的形式,领会起来也是很自然的事情。
甚至,到最后,你慢慢会感觉到,没有什么是不可能的。

什么时候用到?

如果都是自己写的代码,Monkey Patch 就毫无意义了,直接改源码就可以。Monkey Patch 的主要用途,在于 源码不宜直接修改
比如你要修正一个第三方 module 的 bug 或者进行特定的修正、扩展,通常来说有下面几种做法:

  1. 直接把源 package/module,复制一份到当前项目中,再改源码;但不推荐,因为会导致当前的项目代码管理上的混乱。
  2. 向源作者提 pull request,以修正 bug 或者其他;应该如此,但未必能联系到对方,联系到了对方未必修改,修改了未必很快能用上。

这个时候 Monkey Patch 的价值就出来了,不用改原始的 module 源码,就能达到自己期望的效果。

如何打补丁

一个实际例子更帮助理解,看代码之前,我们再回顾一下,之前反复强调 『 module 也是一个变量』

参考源码:

import re
old_match = re.match # 保留旧函数,后面可以直接用

def new_match(*args, **kwargs):
    print('re.match is running') # 增加自己的逻辑
    return old_match(*args, **kwargs) # 调用回旧函数

# 一个全新的函数
def hello(*args, **kwargs):
    print(args, kwargs)

re.match = new_match # 替换掉原来的旧属性(函数)
re.hello = hello # 赋予一个新属性(函数)

直接运行的效果:

>>> re.match('.*', 'abc')
re.match is running # 如果没有 patch,此行不会输出
<re.Match object; span=(0, 3), match='abc'>
>>> re.hello(123) # 如果没有 patch,调用这个函数会报错
(123,) {}

实际使用时,一般会把需要 patch 的逻辑,放在一个多个(归属同一个 module 文件夹) .py 文件内,然后在主要 .py 文件 头部,进行一次 import,这样就实现了全局的 Monkey Patch 了。后续你会看到一些 Gevent 的应用实例,首行基本都进行了 Monkey Patch,也可作为参考。

感觉 Monkey Patch 可以为所欲为?一定程度上的确如此,我们进一步把系统原生数据类型也 patch 掉?

>>> dict.hello = hello
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can't set attributes of built-in/extension type 'dict'