82

我无法用另一个函数替换来自不同模块的函数,这让我发疯。

假设我有一个看起来像这样的模块 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!

我究竟做错了什么?

4

5 回答 5

93

想想 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.'
于 2010-03-03T22:19:45.843 回答
21

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

还有dectools包,我看到了一个 PyCon 2010,它也可以在这种情况下使用,但实际上可能是相反的(方法声明级别的猴子补丁......你不是)

于 2010-03-03T22:20:30.123 回答
10

如果您只想为您的通话打补丁,否则保留原始代码,您可以使用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!'
于 2014-09-24T11:51:49.837 回答
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与它没有神奇的联系。该名称仍然指的是它在初始化时查找的函数对象。

最简单但远非理想的方法是更改bar.py​​为

import a_package.baz

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

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

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

使用全局变量(这是从其他模块更改模块级内容的原因)是一件坏事,会导致难以维护、令人困惑、不可测试、不可扩展的代码,其流程难以跟踪。

于 2010-03-03T22:27:17.473 回答
0

do_something_expensivea_function()函数中只是一个指向函数对象的模块命名空间内的变量。当您重新定义模块时,您是在不同的命名空间中进行的。

于 2010-03-04T11:56:26.000 回答