一只猴子如何在 python 中修补一个函数?

Posted

技术标签:

【中文标题】一只猴子如何在 python 中修补一个函数?【英文标题】:How does one monkey patch a function in python? 【发布时间】:2011-01-23 10:36:03 【问题描述】:

我在用另一个函数替换来自不同模块的函数时遇到问题,这让我发疯。

假设我有一个如下所示的模块 bar.py:

from a_package.baz import do_something_expensive

def a_function():
    print do_something_expensive()

我还有另一个看起来像这样的模块:

from bar import a_function
a_function()

from a_package.baz import do_something_expensive
do_something_expensive = lambda: 'Something really cheap.'
a_function()

import a_package.baz
a_package.baz.do_something_expensive = lambda: 'Something really cheap.'
a_function()

我希望得到结果:

Something expensive!
Something really cheap.
Something really cheap.

但是我得到了这个:

Something expensive!
Something expensive!
Something expensive!

我做错了什么?

【问题讨论】:

第二个不起作用,因为您只是在本地范围内重新定义了 do_something_expensive 的含义。但是,我不知道为什么第三个不起作用... 正如 Nicholas 解释的那样,您正在复制参考并仅替换其中一个参考。由于这个原因,from module import non_module_member 和模块级猴子补丁不兼容,通常最好避免使用。 首选包命名方案是小写,不带下划线,即apackage @bobince,最好避免像这样的模块级可变状态,因为全局变量的不良后果早已被认识到。但是,from foo import bar 很好,实际上在适当的时候推荐。 【参考方案1】:

a_function() 函数中的do_something_expensive 只是模块命名空间中指向函数对象的变量。当您重新定义模块时,您是在不同的命名空间中进行的。

【讨论】:

【参考方案2】:

如果您只想为您的调用打补丁,否则保留原始代码,您可以使用https://docs.python.org/3/library/unittest.mock.html#patch(自 Python 3.3 起):

with patch('a_package.baz.do_something_expensive', new=lambda: 'Something really cheap.'):
    print do_something_expensive()
    # prints 'Something really cheap.'

print do_something_expensive()
# prints 'Something expensive!'

【讨论】:

【参考方案3】:

在第一个 sn-p 中,您使 bar.do_something_expensive 引用了 a_package.baz.do_something_expensive 在那个时刻引用的函数对象。要真正“monkeypatch”,您需要更改函数本身(您只是更改名称所指的内容);这是可能的,但您实际上并不想这样做。

在您尝试改变a_function 的行为时,您做了两件事:

    在第一次尝试中,您将 do_something_expensive 设置为模块中的全局名称。但是,您正在调用a_function,它不会查看您的模块来解析名称,因此它仍然引用相同的函数。

    在第二个示例中,您更改了 a_package.baz.do_something_expensive 所指的内容,但 bar.do_something_expensive 并没有神奇地与之绑定。该名称仍然指的是它在初始化时查找的函数对象。

最简单但远非理想的方法是将bar.py改为say

import a_package.baz

def a_function():
    print a_package.baz.do_something_expensive()

正确的解决方案可能是以下两种情况之一:

重新定义 a_function 以将函数作为参数并调用它,而不是试图潜入并更改硬编码引用的函数,或者 将要使用的函数存储在类的实例中;这就是我们在 Python 中做可变状态的方式。

使用全局变量(这是从其他模块更改模块级内容的原因)是一个坏事,它会导致难以维护、令人困惑、不可测试、不可扩展的代码,其流程难以跟踪。

【讨论】:

【参考方案4】:

有一个非常优雅的装饰器:Guido van Rossum: Python-Dev list: Monkeypatching Idioms。

还有dectools 包,我看到了一个 PyCon 2010,它可能也可以在这种情况下使用,但实际上可能是相反的(在方法声明级别的猴子补丁......你在哪里'不是)

【讨论】:

那些装饰器似乎不适用于这种情况。 @MikeGraham:Guido 的电子邮件没有提到他的示例代码还允许替换任何方法,而不仅仅是添加一个新方法。所以,我认为他们确实适用于这个案例。 @MikeGraham Guido 示例确实非常适合模拟方法语句,我只是自己尝试过! setattr 只是一种花哨的说法 '=' ;所以'a = 3'要么创建一个名为'a'的新变量并将其设置为3,要么用3替换现有变量的值【参考方案5】:

想想 Python 命名空间的工作原理可能会有所帮助:它们本质上是字典。所以当你这样做时:

from a_package.baz import do_something_expensive
do_something_expensive = lambda: 'Something really cheap.'

这样想:

do_something_expensive = a_package.baz['do_something_expensive']
do_something_expensive = lambda: 'Something really cheap.'

希望你能明白为什么这不起作用:-) 一旦你将一个名字导入命名空间,你在 from 导入的命名空间中的名字的值就无关紧要了。您只是在上面的本地模块的命名空间或 a_package.baz 的命名空间中修改 do_something_expensive 的值。但是因为 bar 直接导入 do_something_expensive,而不是从模块命名空间中引用它,所以需要写入它的命名空间:

import bar
bar.do_something_expensive = lambda: 'Something really cheap.'

【讨论】:

第三方包呢,比如here?

以上是关于一只猴子如何在 python 中修补一个函数?的主要内容,如果未能解决你的问题,请参考以下文章

猴子修补python实例方法[重复]

Python 3:猴子补丁代码不能通过多处理重新导入

在 Python 中使用 super.__init__ 进行猴子修补

Python函数的多种解决方案

您如何检测到 Ruby 中发生了猴子补丁?

如何从插件中修补南方处理的模型?