pythonpython-3.xmonkeypatchingdynamic-typing

How to monkeypatch dunder methods to existing instances?


Context: I'd like to use heapq (and anything else) on objects I didn't create, which don't themselves have a __lt__ operator. Can I? (without a wrapper class).

the class:

class Node:
    def __init__(self, val):
        self.val = val

Now, at runtime in the interpreter, I am handed some collection of objects. I want to iterate over them, adding a dunder method (in my case lt), eg:

n = Node(4)
m = Node(5)

def myLT(self, other):
    return self.val < other.val

What I tried:

n.__lt__ = types.MethodType(myLT, n)
m.__lt__ = types.MethodType(myLT, m)

also

n.__lt__ = types.MethodType(myLT, n)
m.__lt__ = types.MethodType(myLT, n)

(on the off chance that binding the same functor-thing would improve matters)

>>> n < m
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: '<' not supported between instances of 'Node' and 'Node'

even though:

>>> n.__lt__(m)
True

I can use a wrapper class, which is yucky in some ways (extra memory and traversal code gets uglier, but at least leaves the original objects untouched):

class NodeWrapper:
    def __init__(self, n):
        self.node = n
    def __lt__(self):
        return self.node.val

I'm just interested to know if I'm doing something wrong in adding the dunder method, or if this just doesn't work in python 3.x. I'm using 3.6.9 if that matters.


Solution

  • You can try monkeypatching the dunder by changing the __class__ property of the instance. As explained in by docs section Special method lookup:

    For custom classes, implicit invocations of special methods are only guaranteed to work correctly if defined on an object’s type, not in the object’s instance dictionary.


    def patch_call(instance, func, memo={}):
        if type(instance) not in memo:
            class _(type(instance)):
                def __lt__(self, *arg, **kwargs):
                   return func(self, *arg, **kwargs)
            memo[type(instance)] = _
    
        instance.__class__ = memo[type(instance)]
    
    patch_call(m, myLT)
    patch_call(n, myLT)
    
    n < m
    # True
    

    Modified from reference.

    Thanks to @juanpa.arrivilaga for recommending the classes be cached to improve performance.