【问题标题】:Monkey patching a class in another module in Python猴子在Python的另一个模块中修补一个类
【发布时间】:2013-11-01 23:35:16
【问题描述】:

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

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

有什么方法可以修改SomeClass__init__ 方法,以便在从mymodule.py 调用dosomething 时,例如打印43 而不是42?理想情况下,我可以包装现有的方法。

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

编辑 2013-10-24

在上面的示例中,我忽略了一个小而重要的细节。 SomeClassthirdpartymodule_b 导入,如下所示:from thirdpartymodule_a import SomeClass

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

【问题讨论】:

  • 我不明白为什么你从哪里调用课程会有所不同。
  • 文件名应该是thirdpartymodule_a.py, thirdpartymodule_b.py

标签: python class unit-testing monkeypatching


【解决方案1】:

以下应该有效:

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

【讨论】:

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

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

【讨论】:

  • 这是唯一能真正正常工作的方法。这基本上是在您拨打电话时进行猴子补丁,做某事,然后撤消猴子补丁。这样,调用它的其他模块仍然会获得原始行为;只有你得到修改后的行为。 (感谢您指出模拟!)
  • @CorleyBrigman 这仅适用于同一进程中的其他模块。对我来说,“其他脚本”听起来像是独立的 Python 进程,不会受到天真的猴子补丁的影响。
【解决方案3】:

另一种可能的方法,与Andrew Clark's one 非常相似,是使用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()

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

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

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

【讨论】:

    【解决方案4】:

    很脏,但它有效:

    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
    

    【讨论】:

    • 如果我希望我的新类定义扩展旧类定义(通过继承)怎么办?
    • 为什么不从SomeClass继承?
    【解决方案5】:

    一个稍微不那么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
    

    当 sentinel 为 false 时,它​​的行为与之前完全相同。如果它是真的,那么你就会得到你的新行为。在你的代码中,你会这样做:

    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 值,这将保持旧的行为。但是您的代码现在有一种方法可以告诉整个堆栈,新行为是可用的。

    【讨论】:

      【解决方案6】:

      这是我使用pytest 为monkeypatch Popen 提出的一个示例。

      导入模块:

      # 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 会覆盖 helpers 中的内置 Popen 类导入:

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

      【讨论】:

        猜你喜欢
        • 2014-08-29
        • 1970-01-01
        • 2011-01-23
        • 1970-01-01
        • 1970-01-01
        • 2014-03-22
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多