我在使用其他功能替换不同模块的功能时遇到了麻烦,这让我发疯了。
假设我有一个模块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 :(得分: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
的行为时,您已做了两件事:
在第一次尝试中,您将do_something_expensive作为模块中的全局名称。但是,您正在调用a_function
,它不会在您的模块中查找名称,因此它仍然引用相同的函数。
在第二个示例中,您更改了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
以将函数作为参数并调用它,而不是试图潜入并更改硬编码引用的函数,或者使用全局变量(这是从其他模块更改模块级别的东西)是一个坏事,导致难以跟踪的流程难以维护,混乱,无法测试,不可扩展的代码。
答案 4 :(得分:0)
do_something_expensive
函数中的 a_function()
只是指向函数对象的模块命名空间内的变量。重新定义模块时,您将在不同的命名空间中执行此操作。