一个猴子在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
的行为时,你做了两件事:
-
在第一次尝试中,您在您的模块中使do_something_expensive成为全局名称。 但是,您正在调用
a_function
,它不会查找您的模块来parsing名称,所以它仍然引用相同的函数。 -
在第二个示例中,您更改了
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。 当您重新定义模块时,您正在使用不同的命名空间。