Search code examples
pythonmonkeypatching

Monkeypatching: replacing method on class to function


I have some silly code with monkey matching part in it. The example below is only for self-studying not for production.

class MyClass:

    def some_method(self):
        print("some_method call")
        self.yet_another_method()

    def yet_another_method(self):
        print('yet_another_method call')


def some_function(self):
    print("some function call")
    self.yet_another_method()

obj = MyClass()
obj.some_method()
obj.some_method = some_function
obj.some_method()

When I execute this code I get the following error:

TypeError: non_class_some_method() missing 1 required positional argument: 'self'

It's obvious that Python interpreter can't implicitly pass obj in some_function. But when I doing introspection and getting the byte code I have the similar representation of method (before and after replace).

import dis
import inspect


class MyClass:

    def some_method(self):
        print("some_method call")
        self.yet_another_method()

    def yet_another_method(self):
        print('yet_another_method call')


def some_function(self):
    print("some function call")
    self.yet_another_method()

obj = MyClass()
dis.dis(obj.some_method)
print(inspect.getargspec(obj.some_method))
obj.some_method = some_function
print("======================================================================")
dis.dis(obj.some_method)
print(inspect.getargspec(obj.some_method))

The result:

  8           0 LOAD_GLOBAL              0 (print) 
              3 LOAD_CONST               1 ('some_method call') 
              6 CALL_FUNCTION            1 (1 positional, 0 keyword pair) 
              9 POP_TOP              

  9          10 LOAD_FAST                0 (self) 
             13 LOAD_ATTR                1 (yet_another_method) 
             16 CALL_FUNCTION            0 (0 positional, 0 keyword pair) 
             19 POP_TOP              
             20 LOAD_CONST               0 (None) 
             23 RETURN_VALUE         
ArgSpec(args=['self'], varargs=None, keywords=None, defaults=None)
======================================================================
 16           0 LOAD_GLOBAL              0 (print) 
              3 LOAD_CONST               1 ('some function call') 
              6 CALL_FUNCTION            1 (1 positional, 0 keyword pair) 
              9 POP_TOP              

 17          10 LOAD_FAST                0 (self) 
             13 LOAD_ATTR                1 (yet_another_method) 
             16 CALL_FUNCTION            0 (0 positional, 0 keyword pair) 
             19 POP_TOP              
             20 LOAD_CONST               0 (None) 
             23 RETURN_VALUE         
ArgSpec(args=['self'], varargs=None, keywords=None, defaults=None)

Please can anyone explain why it happens this way?


Solution

  • Most of the answer is here : https://wiki.python.org/moin/FromFunctionToMethod. To make a long story short:

    • def always yield a function object
    • a method is just a thin callable wrapper around the function, class and instance
    • this wrapper is only created when the function is an attribute of the class - not when it's an attribute of the instance.

    To make your code work (monkeypatching on a per-instance basis), you have to manually invoke the mechanism that would create a method from the function, class and instance. When using new-style classes the simplest solution is to directly invoke the descriptor protocol on the function, ie:

    obj.some_method = some_function.__get__(obj, type(obj))
    

    When using old-style classes like you do in your example (and which is not a good idea), you can use types.MethodType instead, but really unless you are stuck with some legacy code you'd be better making your class a new-style one (=> inherit from object).