programing tip

Python의 다른 모듈에서 클래스를 패치하는 Monkey

itbloger 2020. 12. 8. 07:48
반응형

Python의 다른 모듈에서 클래스를 패치하는 Monkey


다른 사람이 작성한 모듈을 사용하고 있습니다. __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.dosomething()

mymodule.py에서 호출 될 때 예를 들어 42 대신 43을 인쇄 하도록 __init__메서드 를 수정하는 방법이 있습니까? 이상적으로는 기존 방법을 래핑 할 수 있습니다.SomeClassdosomething

다른 스크립트가 기존 기능에 의존하기 때문에 thirdpartymodule * .py 파일을 변경할 수 없습니다. 변경해야하는 변경 사항이 매우 간단하므로 모듈의 자체 복사본을 만들 필요가 없습니다.

2013-10-24 수정

위의 예에서 작지만 중요한 세부 사항을 간과했습니다. SomeClass다음 thirdpartymodule_b과 같이 가져옵니다 from thirdpartymodule_a import SomeClass..

FJ I에 의해 제안 된 패치를하려면에 복사를 교체해야 thirdpartymodule_b보다는 thirdpartymodule_a. 예 : thirdpartymodule_b.SomeClass.__init__ = new_init.


다음이 작동합니다.

import thirdpartymodule_a
import thirdpartymodule_b

def new_init(self):
    self.a = 43

thirdpartymodule_a.SomeClass.__init__ = new_init

thirdpartymodule_b.dosomething()

새 초기화가 이전 초기화를 호출하도록하려면 new_init()정의를 다음으로 바꿉니다.

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

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

더럽지 만 작동합니다.

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

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

또는 때로는 wrap_function_wrapper데코레이터가 아니지만 같은 방식으로 작동하는 것을 사용하고 싶을 수도 있습니다 .

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

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

약간 덜 해킹 된 버전 중 하나는 전역 변수를 매개 변수로 사용합니다.

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이 거짓이면 이전과 똑같이 작동합니다. 그것이 사실이라면, 당신은 새로운 행동을 얻습니다. 코드에서 다음을 수행합니다.

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

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.


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

import the module:

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

A MockBytes object:

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 MockPopen factory to collect the mock 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.fixture it overrides the builtin Popen class import within 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']

참고URL : https://stackoverflow.com/questions/19545982/monkey-patching-a-class-in-another-module-in-python

반응형