猴子在Python中修补另一个模块中的一个类

Sno*_*gus 61 python unit-testing monkeypatching class

我正在使用其他人编写的模块.我想修补__init__模块中定义的类的方法.我发现如何执行此操作的示例都假设我自己会调用该类(例如,Monkey-patch Python类).然而,这种情况并非如此.在我的情况下,该类在另一个模块中的函数内被初始化.请参阅下面的(大大简化)示例:

thirdpartymodule_a.py

class SomeClass(object):
    def __init__(self):
        self.a = 42
    def show(self):
        print self.a
Run Code Online (Sandbox Code Playgroud)

thirdpartymodule_b.py

import thirdpartymodule_a
def dosomething():
    sc = thirdpartymodule_a.SomeClass()
    sc.show()
Run Code Online (Sandbox Code Playgroud)

mymodule.py

import thirdpartymodule_b
thirdpartymodule.dosomething()
Run Code Online (Sandbox Code Playgroud)

有没有办法修改__init__方法,SomeClass以便dosomething从mymodule.py调用它时,例如,打印43而不是42?理想情况下,我能够包装现有方法.

我无法更改thirdpartymodule*.py文件,因为其他脚本依赖于现有功能.我宁愿不必创建我自己的模块副本,因为我需要做的改变非常简单.

编辑2013-10-24

我忽略了上面例子中的一个小而重要的细节.SomeClass是这样导入的thirdpartymodule_b:from thirdpartymodule_a import SomeClass.

要做FJ建议的补丁我需要替换副本thirdpartymodule_b,而不是thirdpartymodule_a.例如thirdpartymodule_b.SomeClass.__init__ = new_init.

And*_*ark 68

以下应该有效:

import thirdpartymodule_a
import thirdpartymodule_b

def new_init(self):
    self.a = 43

thirdpartymodule_a.SomeClass.__init__ = new_init

thirdpartymodule_b.dosomething()
Run Code Online (Sandbox Code Playgroud)

如果您希望新的init调用旧的init new_init(),请使用以下内容替换定义:

old_init = thirdpartymodule_a.SomeClass.__init__
def new_init(self, *k, **kw):
    old_init(self, *k, **kw)
    self.a = 43
Run Code Online (Sandbox Code Playgroud)

  • 也许你应该包括对旧`__init__`的调用. (3认同)
  • @JonathonReinhart 你可能是对的,但我认为 OP 并不真的想在他自己的代码中用 43 替换 42。他特别询问了猴子补丁 (3认同)
  • 似乎从`SomeClass` 继承并替换该类比弄乱`__init__` 函数本身要优雅得多。 (2认同)

fal*_*tru 39

使用mock库.

import thirdpartymodule_a
import thirdpartymodule_b
import mock

def new_init(self):
    self.a = 43

with mock.patch.object(thirdpartymodule_a.SomeClass, '__init__', new_init):
    thirdpartymodule_b.dosomething() # -> print 43
thirdpartymodule_b.dosomething() # -> print 42
Run Code Online (Sandbox Code Playgroud)

要么

import thirdpartymodule_b
import mock

def new_init(self):
    self.a = 43

with mock.patch('thirdpartymodule_a.SomeClass.__init__', new_init):
    thirdpartymodule_b.dosomething()
thirdpartymodule_b.dosomething()
Run Code Online (Sandbox Code Playgroud)

  • 这是实际正常工作的唯一方法.当你打电话时,这基本上是猴子补丁,做了一些事情,然后取消猴子补丁.这样,调用它的其他模块仍然可以获得原始行为; 只有你得到修改过的行为.(并感谢指出模拟!) (6认同)

Mar*_*oft 13

另一种可能的方法与Andrew Clark 的方法非常相似,是使用wrapt库。除了其他有用的东西之外,这个库还提供了wrap_function_wrapperpatch_function_wrapper帮助器。它们可以这样使用:

import wrapt
import thirdpartymodule_a
import thirdpartymodule_b

@wrapt.patch_function_wrapper(thirdpartymodule_a.SomeClass, '__init__')
def new_init(wrapped, instance, args, kwargs):
    # here, wrapped is the original __init__,
    # instance is `self` instance (it is not true for classmethods though),
    # args and kwargs are tuple and dict respectively.

    # first call original init
    wrapped(*args, **kwargs)  # note it is already bound to the instance
    # and now do our changes
    instance.a = 43

thirdpartymodule_b.do_something()
Run Code Online (Sandbox Code Playgroud)

或者有时您可能想使用wrap_function_wrapper它不是装饰器,但其他方式的工作方式相同:

def new_init(wrapped, instance, args, kwargs):
    pass  # ...

wrapt.wrap_function_wrapper(thirdpartymodule_a.SomeClass, '__init__', new_init)
Run Code Online (Sandbox Code Playgroud)