Friday 28 January 2022

Python Bound-Methods and Monkey-Patching

After almost 14 years without hardly any contact with Python (just some occasional reading) I'm coming back to it. In the last years for any scripting needs I'd been sticking to nodejs, but I've recently been assigned to a project at work that uses Python 3, so I'm already trying to get up to date with it.

I've found out today about "bound methods" that I think did not exist in Python 2, and work in an interesting way. I'll compare in this post method invocation in Python and JavaScript, and monkey-patching.

In Javascript, when we invoke a method (either a method defined in a class, and hence added to the class.prototype, or a function directly attached to an instance), I mean:


class Person{
	constructor(name){
		this.name = name;
	}

	sayHi(){
		console.log("Bonjour, je m'appelle " + this.name);
	}
}

let p1 = new Person("Francois");
p1.sayHi();

p1.saySomething = function(msg){console.log(this.name + " vous dit " + msg);};
p1.saySomething();


what is happening in each call are 2 things: first, we lookup the sayHi or saySomething function in the p1 object (its prototype chain... vous savez...) and then, javascript invokes that function passing as this the p1 object on which the look up has been done. The function has a "dynamic this".

Additionally, if the function obtained in the lookup already were bound to a "this" value (either cause it's the result of a Function.prototype.bind or because it's an arrow function), it will be invoked with the bound "this" (static this)

Python 3 works a bit differently, as it comes with the concept of bound methods and the descriptor protocol. Invoking a method also involves 2 steps. First, looking up the function in the object, and then calling that retrieved function. The difference is that if the retrieved function is in the class, what we obtain is a "bound method", as it comes with the object on which the lookup has been done bound as first parameter (there is not "this" notion in Python, we use the "self" convention to refer to the first parameter passed in a function call)


class Person:
    def __init__(self, name, lastName):
        self.name = name
        self.lastName = lastName

    def getFullName(self):
        return self.name + " " + self.lastName

p1 = Person("Francois", "Arboleya")


gFN2 = p1.getFullName
print(gFN2)
#gFN2 is a bound method (0x000001C4338B5FD0 is p1):
#<bound method Person.getFullName of <__main__.Person object at 0x000001C4338B5FD0>>
#so I don't have to pass the person instance, it's already bound
print(gFN2())


This means that if we just attach a function to an object, we have a different behaviour from the one in javascript. The lookup will return the function, not a "bound method", so we can not expand (monkey-patch) an object properly that way.


def fullNameWithFormat(p):
    return "[[" + p.name + " " + p.lastName + "]]"

#monkey-patching the instance is problematic
p1.fullNameWithFormat1 = fullNameWithFormat
#because the lookup returns the function, not a bound-method
print(p1.fullNameWithFormat1)
#<function fullNameWithFormat at 0x000001FA6DC6E040>

try:
    print(p1.fullNameWithFormat1())
except BaseException as err:
    print(f"- Unexpected {err=}, {type(err)=}")
    #TypeError: fullNameWithFormat() missing 1 required positional argument: 'p'

However, we can fix this by means of types.MethodType (that works more or less like Function.prototype.bind). We create a bound method that we can attach to the object instance.


#to monkey-patch the instance we have to do this:
p1.fullNameWithFormat2 = types.MethodType(fullNameWithFormat, p1)
print(p1.fullNameWithFormat2)
#bound method:
#<bound method fullNameWithFormat of <__main__.Person object at 0x0000017CCC9B5FD0>>
print(p1.fullNameWithFormat2())

Notice that for monkey-patching the class, we can just attach a function to the class, and then doing a look-up in an instance will return a bound method


#monkey pathing the class works fine
Person.fullNameWithFormat3 = fullNameWithFormat
print(p1.fullNameWithFormat3)
#bound method
#<bound method fullNameWithFormat of <__main__.Person object at 0x0000017CCC9B5FD0>>
print(p1.fullNameWithFormat3())


No comments:

Post a Comment