0

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?

sweco
  • 143
  • 1
  • 8
  • 1
    Please be aware that putting callables onto objects does not make them methods – methods are specifically either functions on a class *to be* accessed through an instance (unbound method), or functions on a class *that are* accessed through an instance (bound method). ``types.MethodType(mean_hello, a)`` explicitly creates a bound method of ``a``; storing or accessing it as ``a.hello`` does not make it aware that should be re-bound on access. – MisterMiyagi Oct 08 '20 at 08:14
  • 1
    You can use copy.deepcopy which makes a deep copy instead of copy.copy which makes a shallow copy, see @MisterMiyagi message for why this happens. The better way to do it would be through class inheritance and override that function there. – mananony Oct 08 '20 at 08:22
  • Although it looks like a behavior specific to a patched method, it's really just https://stackoverflow.com/questions/4794244/how-can-i-create-a-copy-of-an-object-in-python in disguise. – Wups Oct 08 '20 at 08:52
  • Thank you for your comments and explanation. @mananony you're right, I'll create a decorator that'll subclass the original class and override the method in it. I already tested it and it works just fine since the method becomes unbound. I'll write the answer soon. – sweco Oct 08 '20 at 09:01

2 Answers2

0

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
sweco
  • 143
  • 1
  • 8
  • Just a thought but instead of doing it like this, would it not be better to create a single class "MeanInner" outside a function, and then instantiate that one instead of 'Person' ? that would probably be cleaner than monkey patching all instances... Creating a new class every time sounds overkill and you may have to rethink the way your code works. Instead of doing copy.copy you should just create a new instance of that class. – mananony Oct 08 '20 at 09:23
  • You're right in most cases it would help. However as I described in the question I don't have control over the copying since it is happening in library code. I also could've used a simple subclass (as in your solution) but in my case `Person` actually represents a lot of different classes so a decorator pattern is cleaner than subclassing all of them. Also I need some of the instances of a particular class to "be mean" and some not so that's why I used a specific class for each mean instance. – sweco Oct 08 '20 at 13:11
0

Instead of using copy.copy you should probably use classes as they are intended to be used, and create new instances of an inherited class.

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

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


a = MeanInner('English')
c = MeanInner('German')
print(id(a), id(c))  # 139980069269760 139980069269664
a.hello(2)  # 139980069269760 2 times mean hello in English
c.hello(2)  # 139980069269664 2 times mean hello in German
mananony
  • 537
  • 5
  • 10