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

时间:2010-03-03 22:11:39

标签: python monkeypatching

我在使用其他功能替换不同模块的功能时遇到了麻烦,这让我发疯了。

假设我有一个模块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!

我做错了什么?

5 个答案:

答案 0 :(得分:82)

考虑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.'

希望您能够意识到为什么这不起作用:-)将名称导入命名空间后,从导入的命名空间中的名称值无关紧要。您只是在本地模块的命名空间或上面的a_package.baz命名空间中修改do_something_expensive的值。但是因为bar直接导入do_something_expensive,而不是从模块命名空间引用它,所以需要写入其命名空间:

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

答案 1 :(得分:21)

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

还有dectools包,我看到了PyCon 2010,它也可以在这个上下文中使用,但实际上可能会采用另一种方式(在方法声明级别进行monkeypatching ...在哪里你不是)

答案 2 :(得分:4)

如果你只想为你的电话打补丁而留下原始代码,你可以使用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 :(得分:3)

在第一个代码段中,您bar.do_something_expensive引用a_package.baz.do_something_expensive此时引用的函数对象。要真正“monkeypatch”你需要改变功能本身(你只是改变名称所指的);这是可能的,但你实际上并不想这样做。

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

  1. 在第一次尝试中,您将do_something_expensive作为模块中的全局名称。但是,您正在调用a_function,它不会在您的模块中查找名称,因此它仍然引用相同的函数。

  2. 在第二个示例中,您更改了a_package.baz.do_something_expensive引用的内容,但bar.do_something_expensive并未神奇地绑定它。该名称仍然指的是它在启动时查找的函数对象。

  3. 最简单但又不太理想的方法是将bar.py更改为

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

    正确的解决方案可能是两件事之一:

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

    使用全局变量(这是从其他模块更改模块级别的东西)是一个坏事,导致难以跟踪的流程难以维护,混乱,无法测试,不可扩展的代码。

答案 4 :(得分:0)

do_something_expensive函数中的

a_function()只是指向函数对象的模块命名空间内的变量。重新定义模块时,您将在不同的命名空间中执行此操作。