Search code examples
pythonmonkeypatching

Python copy object with monkey patched instance method


After monkey patching an instance method (using types.MethodType) I pass the monkey patched object to a library which copies it (using copy.copy) and calls the monkey patched method. The monkey patched method gets called successfully but the self parameter references to the old (not copied object).

import copy
import types


class Person:
    def __init__(self, lang):
        self.lang = lang

    def hello(self, n):
        print(id(self), f"{n} times hello in {self.lang}")


def mean_hello(self, n):
    print(id(self), f"{n} times mean hello in {self.lang}")


a = Person('English')
b = copy.copy(a)
b.lang = "French"
print(id(a), id(b))  # 139885310130440 139885310130720
a.hello(1)  # 139885310130440 1 times hello in English
b.hello(1)  # 139885310130720 1 times hello in French

a.hello = types.MethodType(mean_hello, a)
c = copy.copy(a)
c.lang = 'German'
print(id(a), id(c))  # 139885310130440 139885310130664
a.hello(2)  # 139885310130440 2 times mean hello in English
c.hello(2)  # 139885310130440 2 times mean hello in English

From the example it can be seen that the last line c.hello(2) calls the monkey patched method with self referencing to a. Why does this happen? Is there a way to avoid this?


Solution

  • As pointed out by @MisterMiyagi, the method created through types.MethodType is bound to the object a (it can only be called on a) and assigning it to object c (by copying) does not make it unbound.

    The solution outlined by @mananony is to subclass Person and override the method in the subclass. This way the method will remain unbound, linked to the subclass and can be called with any object of type MeanInner.

    I put the subclass in a decorator because in my real code Person actually represents a lot of classes with hello method and I want to be able to change all of them.

    def mean(person_cls):
        class MeanInner(person_cls):
            def hello(self, n):
                print(id(self), f"{n} times mean hello in {self.lang}")
    
        return MeanInner
    
    
    a = mean(Person)('English')
    c = copy.copy(a)
    c.lang = 'German'
    print(id(a), id(c))  # 140407794133424 140407794133536
    a.hello(2)  # 140407794133424 2 times mean hello in English
    c.hello(2)  # 140407794133536 2 times mean hello in German