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

我无法用另一个函数replace不同模块中的函数,这让我发疯。

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

我究竟做错了什么?

这可能有助于思考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.' 

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

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

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

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

在第一个代码片段中,您将bar.do_something_expensive指向那个时候a_package.baz.do_something_expensive指向的函数对象。 要真正“monkeypatch”,你需要改变自己的function(你只是改变什么名字引用); 这是可能的,但你实际上并不想这样做。

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

  1. 在第一次尝试中,您在您的模块中使do_something_expensive成为全局名称。 但是,您正在调用a_function ,它不会查找您的模块来parsing名称,所以它仍然引用相同的函数。

  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() 

正确的解决scheme可能是两件事情之一:

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

使用全局variables(这是从其他模块更改模块级别的东西)是一件坏事 ,会导致无法维护,混乱,不可测,不可测的代码难以跟踪。

如果你只想为你的调用修补它,否则保留原来的代码,你可以使用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!' 

a_function()函数中的do_something_expensive只是指向函数对象的模块名称空间中的一个variables。 当您重新定义模块时,您正在使用不同的命名空间。