Search code examples
pythonmonkeypatching

Python: how to monkey patch class method to other class method


I have got the following code:

class A:
    def __init__(self):
        self.a = "This is mine, "

    def testfunc(self, arg1):
        print self.a + arg1

class B:
    def __init__(self):
        self.b = "I didn't think so"
        self.oldtestfunc = A.testfunc
        A.testfunc = self.testfuncPatch

    def testfuncPatch(self, arg):
        newarg = arg + self.b # B instance 'self'
        self.oldtestfunc(self, newarg) # A instance 'self'

instA = A()
instB = B()
instA.testfunc("keep away! ")

I want to do the following:

Some class A consists of a function with arguments. I want to monkey patch this function to a function in class B do some manipulate the arguments and accessing class B's variables, my problem being the patched function actually needs two different 'self' objects, namely the instance of class A as well as the instance of class B.

Is this possible?


Solution

  • the issue is that when you override a class function with an already bound method, trying to bind to other instances just ignore the second instance:

    print(instA.testfunc)
    #<bound method B.testfuncPatch of <__main__.B object at 0x1056ab6d8>>
    

    so the method basically is treated as a staticmethod meaning you would have to call it with the instance as the first argument:

    instA.testfunc(instA,"keep away! ")
    

    I first ran into this issue when trying to import random.shuffle directly into a class to make it a method:

    class List(list):
        from random import shuffle #I was quite surprised when this didn't work at all
    
    a = List([1,2,3])
    print(a.shuffle)
    #<bound method Random.shuffle of <random.Random object at 0x1020c8c18>>
    a.shuffle()
    
    Traceback (most recent call last):
      File "/Users/Tadhg/Documents/codes/test.py", line 5, in <module>
        a.shuffle()
    TypeError: shuffle() missing 1 required positional argument: 'x'
    

    To fix this issue I created a function that can be rebound to a second instance on top of the first:

    from types import MethodType
    
    def rebinder(f):
        if not isinstance(f,MethodType):
            raise TypeError("rebinder was intended for rebinding methods")
        def wrapper(*args,**kw):
            return f(*args,**kw)
        return wrapper
    
    class List(list):
        from random import shuffle
        shuffle = rebinder(shuffle) #now it does work :D
    
    a = List(range(10))
    print(a.shuffle)
    a.shuffle()
    print(a)
    
    #output:
    <bound method rebinder.<locals>.wrapper of [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]>
    [5, 6, 8, 2, 4, 1, 9, 3, 7, 0]
    

    So you can apply this to your situation just as easily:

    from types import MethodType
    
    def rebinder(f):
        if not isinstance(f,MethodType):
            raise TypeError("rebinder was intended for rebinding methods")
        def wrapper(*args,**kw):
            return f(*args,**kw)
        return wrapper
    ...
    
    class B:
        def __init__(self):
            self.b = "I didn't think so"
            self.oldtestfunc = A.testfunc
            A.testfunc = rebinder(self.testfuncPatch) #!! Edit here
    
        def testfuncPatch(selfB, selfA, arg): #take the instance of B first then the instance of A
            newarg = arg + selfB.b
            self.oldtestfunc(selfA, newarg)