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


76

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

thirdpartymodule_a.py

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

thirdpartymodule_b.py

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

mymodule.py

import thirdpartymodule_b
thirdpartymodule_b.dosomething()

有什么方法可以修改的__init__方法,例如,SomeClassdosomething从mymodule.py调用该方法时,它显示43而不是42?理想情况下,我将能够包装现有方法。

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

编辑2013-10-24

在上面的示例中,我忽略了一个很小但很重要的细节。SomeClass是这样导入的thirdpartymodule_bfrom thirdpartymodule_a import SomeClass

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


我不明白为什么从哪里调用课程会有所不同。
Daniel Roseman

1
文件名应该是thirdpartymodule_a.pythirdpartymodule_b.py
falsetru

Answers:


83

以下应该工作:

import thirdpartymodule_a
import thirdpartymodule_b

def new_init(self):
    self.a = 43

thirdpartymodule_a.SomeClass.__init__ = new_init

thirdpartymodule_b.dosomething()

如果要新的init调用旧的init,则将new_init()定义替换为以下内容:

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

3
也许您应该给旧电话打个电话__init__
用户

1
似乎从类继承SomeClass和替换类比弄乱__init__函数本身要优雅得多。
乔纳森·莱因哈特

2
@JonathonReinhart您可能是对的,但我不认为OP真的想在他自己的代码中用42代替42。他特别询问了有关猴子打补丁的问题
阿雷奥内'17

48

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

要么

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

7
这是真正正常工作的唯一方法。当您打电话时,这基本上是猴子打补丁,执行某些操作,然后撤消猴子补丁。这样,其他调用它的模块仍然具有原始的性能。只有您得到修改后的行为。(并且感谢您指出模拟!)
Corley Brigman 2013年

@CorleyBrigman这仅适用于同一过程中的其他模块。对我来说,“其他脚本”听起来像它们是独立的Python进程,不会受到幼稚的猴子补丁的影响。
安德鲁·克拉克

3

安德鲁·克拉克(Andrew Clark)的方法非常相似的另一种可能方法是使用包装库。除其他有用的东西外,该库还提供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()

或者有时您可能想使用wrap_function_wrapper不是装饰器的东西,但其他方法也可以使用相同的方式:

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

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

2

脏,但是可以用:

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

2
如果我希望新的类定义扩展旧的类定义(通过继承)怎么办?
yucer '16

1
为什么不继承SomeClass
Arnie97 '19

1

一个仅有很少改动的版本使用全局变量作为参数:

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

如果哨兵为假,则其行为与以前完全相同。如果这是真的,那么您将获得新的行为。在您的代码中,您将执行以下操作:

import thirdpartymodule_b

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

当然,在不影响现有代码的情况下进行适当的修复是相当琐碎的。但是您必须稍微更改其他模块:

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

并传递给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

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


1

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

导入模块:

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

一个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

一家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

和一个示例测试:

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']

这是相同的示例,但是使用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 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.