2010-03-03 79 views
61

我无法用另一个函数从另一个模块中替换一个函数,这让我发疯。一只猴子在python中如何修补一个函数?

比方说,我有一个模块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! 

我在做什么错?

+0

第二个不能工作,因为你只是重新定义do_something_expensive的意义在你的本地范围内。但我不知道,为什么第三个不工作...... – pajton 2010-03-03 22:18:22

+1

正如尼古拉斯解释的那样,您正在复制一个参考文献并只替换其中的一个参考文献。 '从模块导入non_module_member'和模块级别的猴子打补丁是不兼容的,因为这个原因,通常都是最好的避免。 – bobince 2010-03-03 22:29:35

+0

首选的软件包命名方案是小写的,没有下划线,即'apackage'。 – 2010-03-03 22:39:00

回答

69

这可能有助于思考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的值。但由于巴进口直接do_something_expensive,而不是从模块命名空间引用它,你需要写它的命名空间:

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

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

还有一个dectools包,我看到了一个PyCon 2010,可能也可以在这个上下文中使用,但实际上它可能是另一种方式(monkeypatching在方法声明级别,重新不​​)

+3

这些修饰器似乎不适用于这种情况。 – 2010-03-03 22:37:13

+1

@MikeGraham:Guido的电子邮件没有提及他的示例代码也允许替换任何方法,而不仅仅是添加一个新的方法。所以,我认为他们确实适用于这种情况。 – tuomassalo 2012-03-25 14:15:24

+0

@MikeGraham Guido的例子完全适用于模拟方法声明,我只是自己尝试过! setattr只是说'='的一种奇特的方式;因此,'a = 3'或者创建一个名为'a'的新变量并将其设置为3或用3替换现有变量的值 – 2015-05-22 01:00:12

4

在第一个片段中,您使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与其并不相关。该名称仍然指它在启动时查找的功能对象。

最简单的,但远从理想的办法是改变bar.py

import a_package.baz 

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

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

  • 重新定义a_function以函数作为参数并调用它,而不是试图偷偷摸摸并改变它的功能d编码以指代,或者
  • 存储要在类的实例中使用的函数;这就是我们在Python中如何做可变状态。

使用全局变量(这是从其他模块更换模块级的东西)是坏事导致不可维护的,混乱的,untestestable,不可扩展的代码流是很难跟踪。

0

do_something_expensivea_function()函数只是指向函数对象的模块名称空间内的变量。当你重新定义模块时,你正在使用不同的命名空间。

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!'