Search code examples
python-3.xmonkeypatching

Monkeypatching a Python class


I would like to understand how Python classes and objects work. In Perl it is pretty simple, each sub definied in a package can be called as static, class or object method (CLASS::func, CLASS->func or $obj->func). For the first glance, a Python class looks like a Perl class with a bless-ed HASH (The __dict__ attribute in Python class). But in Python I'm a little bit confused. So, to understand better, I have tried to monkey-patch an empty class, adding 3 attributes which behave exactly like a static, class and object method, but I could not get it.

At first I have created the normal class to get the base result:

def say(msg, x):
    print('*', msg, 'x =', x, 'type(x) =', type(x))


class A():
    @staticmethod
    def stt_m(x):
        say('stt_m', x)

    @classmethod
    def cls_m(x):
        say('cls_m', x)

    def obj_m(x):
        say('obj_m', x)

Then I have created a function (called test) which tries to call all methods with one parameter and if fails (as the first parameter can be the class or object itself), tries to call again with none printing an 'X' in front of the output line, and then prints the detected types:

def test(obj):
    # Detect if obj is a class or an instantiated object
    what = 'Class' if type(obj) == type else 'Object'
    print()
    # Try to call static, class and object method getting attributes
    for a in ('stt_m', 'cls_m', 'obj_m'):
        meth = getattr(obj, a)
        try:
            meth(111)
        except:
            print('X', end='')
            meth()
        print(' ', what, a, meth)

Calling test with the default A class and its object:

test(A)
test(A())

The result is:

* stt_m x = 111 type(x) = <class 'int'>
  Class stt_m <function A.stt_m at 0x7fb37e63c8c8>
X* cls_m x = <class '__main__.A'> type(x) = <class 'type'>
  Class cls_m <bound method A.cls_m of <class '__main__.A'>>
* obj_m x = 111 type(x) = <class 'int'>
  Class obj_m <function A.obj_m at 0x7fb37e63c9d8>

* stt_m x = 111 type(x) = <class 'int'>
  Object stt_m <function A.stt_m at 0x7fb37e63c8c8>
X* cls_m x = <class '__main__.A'> type(x) = <class 'type'>
  Object cls_m <bound method A.cls_m of <class '__main__.A'>>
X* obj_m x = <__main__.A object at 0x7fb37e871748> type(x) = <class '__main__.A'>
  Object obj_m <bound method A.obj_m of <__main__.A object at 0x7fb37e871748>>

So, calling a staticmethod with either class or object prefix, they behaves as normal (namespace) functions (accepting 1 argument). Calling a classmethod with either way, the first argument passed is the class object. Calling objectmethod from a class behaves as a normal function and if called from an object, then the first argument is the object itself. This later looks a bit strange, but I can live with it.

Now, let's try to monkey-patch an empty class:

class B():
    pass

B.stt_m = lambda x: say('stt_m', x)
B.cls_m = types.MethodType(lambda x: say('cls_m', x), B)
B.obj_m = types.MethodType(lambda x: say('obj_m', x), B())

test(B)
test(B())

Result is:

* stt_m x = 111 type(x) = <class 'int'>
  Class stt_m <function <lambda> at 0x7fbf05ec7840>
X* cls_m x = <class '__main__.B'> type(x) = <class 'type'>
  Class cls_m <bound method <lambda> of <class '__main__.B'>>
X* obj_m x = <__main__.B object at 0x7fbf0d7dd978> type(x) = <class '__main__.B'>
  Class obj_m <bound method <lambda> of <__main__.B object at 0x7fbf0d7dd978>>

X* stt_m x = <__main__.B object at 0x7fbf06375e80> type(x) = <class '__main__.B'>
  Object stt_m <bound method <lambda> of <__main__.B object at 0x7fbf06375e80>>
X* cls_m x = <class '__main__.B'> type(x) = <class 'type'>
  Object cls_m <bound method <lambda> of <class '__main__.B'>>
X* obj_m x = <__main__.B object at 0x7fbf0d7dd978> type(x) = <class '__main__.B'>
  Object obj_m <bound method <lambda> of <__main__.B object at 0x7fbf0d7dd978>>

According to this pattern, stt_m behaves, like an object method of the normal class and cls_m and obj_m behaves like class method of the normal class.

Can I monkey-patch a static method this way?


Solution

  • You can monkey-patch methods onto a class, but it’s done like this:

    B.stt_m = staticmethod(lambda x: say('stt_m', x))
    B.cls_m = classmethod(lambda x: say('cls_m', x))
    B.obj_m = lambda x: say('obj_m', x)
    

    Your version for B.cls_m is OK, but your B.stt_m creates a normal method, and your B.obj_m attaches an instance method to a newly created B(), but then that B() is thrown away, and you test a new B() without the extra method.

    There’s usually no need to use types.MethodType in Python:

    types.MethodType(function, object_)
    

    is equivalent to

    function.__get__(object_)
    

    which is a bit better, although also very rare.

    Also (irrelevant but too neat not to mention), in newish versions of Python, your

    print('*', msg, 'x =', x, 'type(x) =', type(x))
    

    can just be written as

    print(f"* {msg} {x = } {type(x) = }")