什么是猴子补丁?

831

我希望你能够协助我理解什么是“monkey patching”或者说“monkey patch”?

它类似于方法/运算符重载或委托吗?

它与这些概念有何共同之处吗?


33
我认为谷歌的定义非常有用且最为通用:Monkey patching 是一种技术,可以在运行时添加、修改或抑制代码片段的默认行为,而无需改变其原始源代码。 - Charlie Parker
8个回答

750
不,它不像任何一个东西,它仅仅是在运行时动态替换属性。
例如,考虑一个具有方法get_data的类。这个方法对外部查找(例如数据库或Web API)进行操作,类中的其他各种方法都会调用它。但是,在单元测试中,您不希望依赖于外部数据源——因此,您可以动态地将get_data方法替换为返回一些固定数据的存根。
由于Python类是可变的,而方法只是类的属性,因此您可以随心所欲地执行此操作——实际上,您甚至可以以完全相同的方式替换模块中的类和函数。
但正如一位评论者指出的那样,当使用Monkeypatching时要小心:
  1. 如果除了测试逻辑之外的任何其他内容也调用了get_data,它也会调用您的猴子补丁替换而不是原始版本,这可能是好事或坏事。只要小心即可。
  2. 如果存在某个变量或属性,该变量或属性也指向get_data函数,那么在您替换它时,这个别名将不会改变其含义,并将继续指向原始的get_data。(为什么?Python只会重新绑定您类中的名称get_data到某个其他函数对象;其他名称绑定则不受任何影响。)

2
@LutzPrechelt 为了让我明确,您所说的“指向原始的get_data函数”是什么意思?您是否意味着当将函数存储在变量中时,如果有人更改该函数,则变量将继续指向旧函数? - fabriciorissetto
12
在Python中,通常不会更改函数对象。当你进行" monkey-patch get_data "时,你是将名称get_data重新绑定到模拟函数上。如果程序中的其他某个名称绑定到了之前被称为“get_data”的函数形式,在该名称处不会发生任何变化。 - Lutz Prechelt
1
@LutzPrechelt,您能在这方面再解释一下吗? - Calvin Ku
我认为猴子补丁在调试、装饰器或对象工厂函数方面非常有用。然而,记住显式优于隐式,因此确保您的代码是上下文无关的,阅读《考虑到goto的有害性》等等... - aoeu256
那么,这就类似于使用“eval”函数,在运行时可以插入新代码吗? - wintermute
你的定义似乎太简单了。谷歌的定义“Monkey patching是一种在运行时添加、修改或抑制代码片段的默认行为的技术,而不改变其原始源代码。”听起来更加灵活,“只重新分配属性”。我可以想象一段返回随机数的代码,然后通过包装原始函数来将数字增加2并进行monkey patching,这不需要任何类或其他东西。 - Charlie Parker

456

猴子补丁是一段 Python 代码,它在运行时(通常在启动时)扩展或修改其他代码。

一个简单的例子如下:

from SomeOtherProduct.SomeModule import SomeClass

def speak(self):
    return "ook ook eee eee eee!"

SomeClass.speak = speak

来源:Zope wiki上的MonkeyPatch页面。


172

什么是猴子补丁?

简单来说,猴子补丁是在程序运行时对模块或类进行更改。

使用示例

Pandas文档中有一个猴子补丁的例子:

import pandas as pd
def just_foo_cols(self):
    """Get a list of column names containing the string 'foo'

    """
    return [x for x in self.columns if 'foo' in x]

pd.DataFrame.just_foo_cols = just_foo_cols # monkey-patch the DataFrame class
df = pd.DataFrame([list(range(4))], columns=["A","foo","foozball","bar"])
df.just_foo_cols()
del pd.DataFrame.just_foo_cols # you can also remove the new method
为了解释清楚,首先我们需要导入这个模块:
import pandas as pd

接下来,我们创建一个方法定义,它存在于任何类定义的范围之外,并且是自由的(由于函数和未绑定方法之间的区别在Python 3中相当无意义,因此取消了未绑定方法):

def just_foo_cols(self):
    """Get a list of column names containing the string 'foo'

    """
    return [x for x in self.columns if 'foo' in x]

接下来,我们只需将该方法附加到我们想要在其上使用它的类即可:

pd.DataFrame.just_foo_cols = just_foo_cols # monkey-patch the DataFrame class

然后我们可以在类的实例上使用这个方法,在完成后删除这个方法:

df = pd.DataFrame([list(range(4))], columns=["A","foo","foozball","bar"])
df.just_foo_cols()
del pd.DataFrame.just_foo_cols # you can also remove the new method

关于名称混淆的警告

如果您使用名称混淆(使用双下划线前缀属性,改变名称,我不建议这样做),则如果这样做,您将不得不手动进行名称混淆。由于我不建议名称混淆,因此我不会在此处进行演示。

测试示例

例如,我们如何在测试中使用这些知识呢?

假设我们需要模拟对外部数据源的数据检索调用,结果出现错误,因为我们想确保在这种情况下正确的行为。我们可以使用猴子补丁来确保这种行为。(所以使用与Daniel Roseman建议的相似的方法名:)

import datasource

def get_data(self):
    '''monkey patch datasource.Structure with this to simulate error'''
    raise datasource.DataRetrievalError

datasource.Structure.get_data = get_data

如果我们测试一个依赖于该方法引发错误的行为,那么如果实现正确,我们将在测试结果中获得该行为。

仅仅执行上述操作将会改变进程的整个生命周期中的Structure对象,因此你需要在你的单元测试中使用设置和清除操作来避免这种情况,例如:

def setUp(self):
    # retain a pointer to the actual real method:
    self.real_get_data = datasource.Structure.get_data
    # monkey patch it:
    datasource.Structure.get_data = get_data

def tearDown(self):
    # give the real method back to the Structure object:
    datasource.Structure.get_data = self.real_get_data

虽然上述方法有效,但使用mock库来修补代码可能是更好的选择。mockpatch装饰器比上述方法更不容易出错,因为它需要更少的代码行,从而减少了引入错误的机会。我还没有审查mock中的代码,但我想它以类似的方式使用Monkey-Patching。


那么,是由Monkeypatcher来存储对真实方法的引用吗?例如,如果忘记了“保留指针”步骤,它会丢失吗? - Tommy
3
如果一个方法的引用计数归零,它被视为"被覆盖"并被垃圾回收程序回收,在进程的生命周期中就会被"丢失"(或者除非它所在的模块被重新加载,但这通常是不推荐的)。 - Russia Must Remove Putin

41
根据维基百科的定义:
在Python中,“猴子补丁”这个术语仅指在运行时对类或模块进行动态修改,旨在修补现有的第三方代码以解决bug或特性问题,因为它们不能按您所期望的方式工作。

21

首先:在我看来,猴子补丁是一种不好的黑客技巧。

它通常用于替换模块或类级别上的方法,使用自定义实现。

最常见的用例是在您无法替换原始代码时,为模块或类中的错误添加解决方法。在这种情况下,您可以通过猴子补丁将“错误”的代码替换为您自己模块/包内部的实现。


9
如果有一些模块都对同一件事进行了猴子补丁:你就完蛋了。 - user2665694
61
虽然它的力量普遍来说可能会很危险,但在测试方面是非常好的。 - dkrikun
3
最常见的用例实际上是测试,特别是单元测试。您希望仅测试您的代码,因此修补任何外部调用以返回预期结果。 - brocoli
7
它并不邪恶,我用它来修补别人软件中的漏洞,直到新版本发布,而不是分支和创建新的依赖关系。 - nurettin
2
猴子补丁可以以“纯函数式”的方式完成,而不是可变的、“上下文敏感”的、类似于goto的方式,只需在返回新修补版本的装饰器内进行修补(而不是修改它)。许多C# / Java程序员不知道REPL驱动开发,因此他们在需要静态定义一切的IDE中编码。由于C# / Java没有猴子补丁,所以当他们在JavaScript、Smalltalk、Lisp、Python等中看到它时,他们认为它是邪恶的,因为它违反了他们的静态IDE驱动开发实践。 - aoeu256
显示剩余3条评论

15

猴子补丁只能在动态语言中实现,其中Python是一个很好的例子。在运行时改变方法而不是更新对象定义是一个例子;同样,在运行时添加属性(无论是方法还是变量)也被视为猴子补丁。通常在使用没有源代码的模块时进行这些操作,以便对象定义不能轻松地更改。

这被认为是不好的,因为它意味着对象的定义并不完全或准确地描述其真正的行为。


1
然而,猴子补丁可以很有用,只要你不是修改现有的对象或类,而是在装饰器内创建一个带有补丁成员的新版本对象,这样就能清晰地表明“嘿,我要给你打补丁”。 - aoeu256
你可以在修补过的成员上使用注释,以将修补过程中使用的装饰器存储到修补程序中。假设你有一个可撤销的装饰器,它创建了一个带有undo方法的函数对象的新可撤销版本。你可以在装饰器中放置一个指向你的可撤销装饰器的patcher字段。 - aoeu256

6

Monkey patching是在运行时重新打开现有类或类中的方法并更改其行为,应该谨慎使用,或者只有在真正需要时才使用。

由于Python是一种动态编程语言,类是可变的,因此可以重新打开它们并进行修改,甚至替换它们。


6

什么是猴子补丁? 猴子补丁是一种在运行时动态更新代码行为的技术。

为什么使用猴子补丁? 它允许我们在运行时修改或扩展库、模块、类或方法的行为,而不必实际修改源代码。

结论 猴子补丁是一种很酷的技术,现在我们已经学会了如何在Python中实现。但是,正如我们讨论过的那样,它也有自己的缺点,应该谨慎使用。


引用的来源是什么? - OneCricketeer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接