Search code examples
pythonmonkeypatching

Patching all instances of a class in Python


I'm on Python 3.5.1. I wrote a library, and under certain circumstances (when the file is run directly, i.e. __name__ == '__main__') I want to decorate certain methods in one of the classes. It should decorate all the instances that can ever be created. I would like to do it in a way that is non invasive, i.e. ideally the class in my library would not need any special code.

After a while, I managed to implement something like this, which meets my requirements:

def patch(clazz, name, replacement):

    def wrap_original(orig):
        # when called with the original function, a new function will be returned
        # this new function, the wrapper, replaces the original function in the class
        # and when called it will call the provided replacement function with the
        # original function as first argument and the remaining arguments filled in by Python

        def wrapper(*args, **kwargs):
            return replacement(orig, *args, **kwargs)

        return wrapper

    orig = getattr(clazz, name)
    setattr(clazz, name, wrap_original(orig))

def replacement_function(orig, self, ... other argumnents ...):
    # orig here is the original function, so can be called like this:
    # orig(self, ... args ...)
    pass

patch(mylib.MyClass, 'method_name', replacemment_function)

Amazingly, this code works, although I haven't tested it with class methods yet, but I don't need it now. It also patches instances created before the patching, although I'm not yet certain whether it's good or not ;d

The code above is arguably difficult, I needed a while to wrap my head around the way it works after I wrote it, in order to write the explanation comment. I would love something easier.

The question: is there anything in the Python library that would make code like this unnecessary, which already implements what I'm doing, but better?


Solution

  • One of the posters here, who sadly deleted her/his post, directed me towards the functools module. In the end, I settled on the following:

    def replacement(self, orig, ... other arguments ...):
        # orig here is the original function, so can be called like this:
        # orig(self, ... args ...)
        pass
    
    mylib.MyClass.my_method = functools.partialmethod(replacement, mylib.MyClass.my_method)
    

    The orig and self arguments needed to switch places, as the result of partialmethod binds the first argument to the instance it is in, and the second in this case will be the original function (the second argument to partialmethod). Looks much cleaner.