一只猴子如何修补python中的功能?


80

我在用另一个功能替换另一个模块中的功能时遇到麻烦,这让我发疯。

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

我究竟做错了什么?


第二个方法不起作用,因为您只是在本地范围内重新定义了do_something_expensive的含义。但是我不知道为什么第三个不起作用...
pajton

1
正如Nicholas解释的那样,您正在复制参考,仅替换其中一个参考。from module import non_module_member由于这个原因,模块级别的猴子补丁和模块级别的猴子补丁是不兼容的,通常最好避免两者。
bobince 2010年

首选的程序包命名方案是小写且没有下划线,即apackage
Mike Graham

1
@bobince,最好避免这样的模块级可变状态,因为全局变量的不良后果早已被认识到。但是,from foo import bar这很好,实际上建议在适当的时候使用。
Mike Graham

Answers:


87

考虑一下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.'

怎么样从第三方软件包,喜欢这里
Tengerye

21

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

还有dectools包,我看到了PyCon 2010,它也可以在此上下文中使用,但是实际上可能是相反的(在方法声明级别进行monpatpatching……不是)


4
这些装饰器似乎不适用于这种情况。
Mike Graham

1
@MikeGraham:Guido的电子邮件中没有提到他的示例代码还允许替换任何方法,而不仅仅是添加一个新方法。因此,我认为它们确实适用于这种情况。
tuomassalo 2012年

@MikeGraham Guido示例确实可以很好地模拟方法声明,我只是亲自尝试了一下!setattr只是说'='的一种奇特方式;因此,“ a = 3”要么创建一个名为“ a”的新变量并将其设置为3,要么将现有变量的值替换为3
Chris Huang-Leaver 2015年

6

如果您只想为呼叫打补丁,或者保留原始代码,则可以使用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

在第一个代码段中,您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中进行可变状态的方式。

使用全局变量(这是从其他模块更改模块级别的东西的做法)是一件坏事,它导致难以维护的,混乱的,不可测试的,不可伸缩的代码,其流程很难跟踪。


0

do_something_expensivea_function()功能仅仅是模块指向一个功能对象的名称空间内的变量。当您重新定义模块时,您将在另一个名称空间中进行操作。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.