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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/19545982/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-19 14:00:48  来源:igfitidea点击:

Monkey patching a class in another module in Python

pythonclassunit-testingmonkeypatching

提问by Snorfalorpagus

I'm working with a module written by someone else. I'd like to monkey patch the __init__method of a class defined in the module. The examples I have found showing how to do this have all assumed I'd be calling the class myself (e.g. Monkey-patch Python class). However, this is not the case. In my case the class is initalised within a function in another module. See the (greatly simplified) example below:

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

thirdpartymodule_a.py

第三方模块_a.py

class SomeClass(object):
    def __init__(self):
        self.a = 42
    def show(self):
        print self.a

thirdpartymodule_b.py

第三方模块_b.py

import thirdpartymodule_a
def dosomething():
    sc = thirdpartymodule_a.SomeClass()
    sc.show()

mymodule.py

我的模块.py

import thirdpartymodule_b
thirdpartymodule.dosomething()

Is there any way to modify the __init__method of SomeClassso that when dosomethingis called from mymodule.py it, for example, prints 43 instead of 42? Ideally I'd be able to wrap the existing method.

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

I can't change the thirdpartymodule*.py files, as other scripts depend on the existing functionality. I'd rather not have to create my own copy of the module, as the change I need to make is very simple.

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

Edit 2013-10-24

编辑 2013-10-24

I overlooked a small but important detail in the example above. SomeClassis imported by thirdpartymodule_blike this: from thirdpartymodule_a import SomeClass.

在上面的例子中,我忽略了一个很小但很重要的细节。SomeClass采用进口thirdpartymodule_b这样的:from thirdpartymodule_a import SomeClass

To do the patch suggested by F.J I need to replace the copy in thirdpartymodule_b, rather than thirdpartymodule_a. e.g. thirdpartymodule_b.SomeClass.__init__ = new_init.

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

采纳答案by Andrew Clark

The following should work:

以下应该工作:

import thirdpartymodule_a
import thirdpartymodule_b

def new_init(self):
    self.a = 43

thirdpartymodule_a.SomeClass.__init__ = new_init

thirdpartymodule_b.dosomething()

If you want the new init to call the old init replace the new_init()definition with the following:

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

old_init = thirdpartymodule_a.SomeClass.__init__
def new_init(self, *k, **kw):
    old_init(self, *k, **kw)
    self.a = 43

回答by falsetru

Use mocklibrary.

使用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

or

或者

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()

回答by lucasg

Dirty, but it works :

脏,但它有效:

class SomeClass2(object):
    def __init__(self):
        self.a = 43
    def show(self):
        print self.a

import thirdpartymodule_b

# Monkey patch the class
thirdpartymodule_b.thirdpartymodule_a.SomeClass = SomeClass2

thirdpartymodule_b.dosomething()
# output 43

回答by Corley Brigman

One only slightly-less-hacky version uses global variables as parameters:

一个稍微少点hacky的版本使用全局变量作为参数:

sentinel = False

class SomeClass(object):
    def __init__(self):
        global sentinel
        if sentinel:
            <do my custom code>
        else:
            # Original code
            self.a = 42
    def show(self):
        print self.a

when sentinel is false, it acts exactly as before. When it's true, then you get your new behaviour. In your code, you would do:

当 sentinel 为 false 时,它​​的行为与以前完全一样。如果它是真的,那么你就会得到你的新行为。在您的代码中,您将执行以下操作:

import thirdpartymodule_b

thirdpartymodule_b.sentinel = True    
thirdpartymodule.dosomething()
thirdpartymodule_b.sentinel = False

Of course, it is fairly trivial to make this a proper fix without impacting existing code. But you have to change the other module slightly:

当然,在不影响现有代码的情况下进行适当的修复是相当简单的。但是你必须稍微改变另一个模块:

import thirdpartymodule_a
def dosomething(sentinel = False):
    sc = thirdpartymodule_a.SomeClass(sentinel)
    sc.show()

and pass to init:

并传递给init:

class SomeClass(object):
    def __init__(self, sentinel=False):
        if sentinel:
            <do my custom code>
        else:
            # Original code
            self.a = 42
    def show(self):
        print self.a

Existing code will continue to work - they will call it with no arguments, which will keep the default false value, which will keep the old behaviour. But your code now has a way to tell the whole stack on down that new behaviour is available.

现有代码将继续工作 - 他们将不带参数调用它,这将保留默认的 false 值,这将保留旧的行为。但是您的代码现在有一种方法可以告诉整个堆栈有新的行为可用。

回答by jmunsch

Here is an example I came up with to monkeypatch Popenusing pytest.

这里是我想出了到猴补丁的例子Popen使用pytest

import the module:

导入模块:

# must be at module level in order to affect the test function context
from some_module import helpers

A MockBytesobject:

一个MockBytes对象:

class MockBytes(object):

    all_read = []
    all_write = []
    all_close = []

    def read(self, *args, **kwargs):
        # print('read', args, kwargs, dir(self))
        self.all_read.append((self, args, kwargs))

    def write(self, *args, **kwargs):
        # print('wrote', args, kwargs)
        self.all_write.append((self, args, kwargs))

    def close(self, *args, **kwargs):
        # print('closed', self, args, kwargs)
        self.all_close.append((self, args, kwargs))

    def get_all_mock_bytes(self):
        return self.all_read, self.all_write, self.all_close

A MockPopenfactory to collect the mock popens:

一个MockPopen收集模拟 popens 的工厂:

def mock_popen_factory():
    all_popens = []

    class MockPopen(object):

        def __init__(self, args, stdout=None, stdin=None, stderr=None):
            all_popens.append(self)
            self.args = args
            self.byte_collection = MockBytes()
            self.stdin = self.byte_collection
            self.stdout = self.byte_collection
            self.stderr = self.byte_collection
            pass

    return MockPopen, all_popens

And an example test:

和一个示例测试:

def test_copy_file_to_docker():
    MockPopen, all_opens = mock_popen_factory()
    helpers.Popen = MockPopen # replace builtin Popen with the MockPopen
    result = copy_file_to_docker('asdf', 'asdf')
    collected_popen = all_popens.pop()
    mock_read, mock_write, mock_close = collected_popen.byte_collection.get_all_mock_bytes()
    assert mock_read
    assert result.args == ['docker', 'cp', 'asdf', 'some_container:asdf']

This is the same example, but using pytest.fixtureit overrides the builtin Popenclass import within helpers:

这是同一个示例,但使用pytest.fixture它会覆盖 内的内置Popen类导入helpers

@pytest.fixture
def all_popens(monkeypatch): # monkeypatch is magically injected

    all_popens = []

    class MockPopen(object):
        def __init__(self, args, stdout=None, stdin=None, stderr=None):
            all_popens.append(self)
            self.args = args
            self.byte_collection = MockBytes()
            self.stdin = self.byte_collection
            self.stdout = self.byte_collection
            self.stderr = self.byte_collection
            pass
    monkeypatch.setattr(helpers, 'Popen', MockPopen)

    return all_popens


def test_copy_file_to_docker(all_popens):    
    result = copy_file_to_docker('asdf', 'asdf')
    collected_popen = all_popens.pop()
    mock_read, mock_write, mock_close = collected_popen.byte_collection.get_all_mock_bytes()
    assert mock_read
    assert result.args == ['docker', 'cp', 'asdf', 'fastload_cont:asdf']

回答by MarSoft

One another possible approach, very similar to Andrew Clark's one, is to use wraptlibrary. Among other useful things, this library provides wrap_function_wrapperand patch_function_wrapperhelpers. They can be used like this:

另一种可能的方法,与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()

Or sometimes you may want to use wrap_function_wrapperwhich is not a decorator but othrewise works the same way:

或者有时你可能想使用wrap_function_wrapperwhich 不是装饰器,但以同样的方式工作:

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

wrapt.wrap_function_wrapper(thirdpartymodule_a.SomeClass, '__init__', new_init)