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)
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 + "]]"
p1.fullNameWithFormat1 = fullNameWithFormat
print(p1.fullNameWithFormat1)
try:
print(p1.fullNameWithFormat1())
except BaseException as err:
print(f"- Unexpected {err=}, {type(err)=}")
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.
p1.fullNameWithFormat2 = types.MethodType(fullNameWithFormat, p1)
print(p1.fullNameWithFormat2)
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
Person.fullNameWithFormat3 = fullNameWithFormat
print(p1.fullNameWithFormat3)
print(p1.fullNameWithFormat3())