Saturday 30 April 2022

Python Bound Methods Part - 2

This post is a follow-up to this one. There I talked about how invoking a method through an instance of a class will search for that method in the class dictionary and return a bound method, that is, the method with the instance object bound to it, so that it will be passed as first parameter (what we usually call "self"). We'll see now some related details.

In python we have 2 options when declaring a method that does not use an instance of the class (has no "self), what in most languages we call static or class methods:
the @staticmethod decorator and the @classmethod decorator.
In both cases the method can be invoked through the class itself or an instance of that class, I mean MyClass.method() or my_instance.method().

The difference comes from whether the method needs to invoke other methods (classmethods or staticmethods) in the class. In the first case, you have to declare the method as a classmethod, so that it will receive as first parameter the class it belongs to. In the second case you declare it as staticmethod, and you will only receive the parameters that you have passed in the invocation.

My understanding of how classmethods invocation happens is like this. When invoking it through an instance, the lookup mechanism will search the method first (to not avail) in the instance.__dict__ , and then will search it (and find it) in the class.__dict__. Obviously, when invoked through the class, it'll directly search in the class.__dict__. Then, as the method has been decorated with @classmethod, a bound method object will be created (bound to the class) and returned (something like: bound method Person.sayHi of class '__main__.Person'). So when a method is retrieved from a class dictionary, if it's a @classmethod we get it bound to the class, if it's a @staticmethod we get it unbound, and if it's an instance method we get it bound to that instance if accessed through an instance, or unbound if accessed through the class. I mean:


class Person:
    PLANET = "Earth"
    def __init__(self, name):
        self.name = name
    
    def say_hi(self):
        print(f"Person {self.name} says hi")

    @classmethod
    def cls_meth(cls):
        print(cls.PLANET)

    @staticmethod
    def static_meth(msg):
        print(f"msg: {msg}")


p1 = Person("Francois")
print(Person.cls_meth) # bound method Person.cls_meth of class '__main__.Person'
print(p1.cls_meth) # bound method Person.cls_meth of class '__main__.Person'

print(Person.static_meth) # function Person.static_meth at 0x7f2bdddfd040
print((p1.static_meth)) # function Person.static_meth at 0x7f2bdddfd040

print(Person.say_hi) # function Person.say_hi at 0x7f2bdddf4ee0
print(p1.say_hi) # bound method Person.say_hi of __main__.Person object at 0x7f2bdde97730

As I've said, if we retrieve an instance method through the class rather than through an instance we get an unbound method. That unbound method is expecting to receive a "self", so we'll have to pass it along with the other parameters. We could pass an instance of a different class in that "self" (and thanks to duck typing if it has the properties expected by that method it will work).


class Book:
    def __init__(self, name):
        self.name = name

b1 = Book("Guerilla")
Person.say_hi(a1)

# "Person Guerilla says hi

We can do the same in JavaScript (using Array methods with "array like" objects like arguments easily comes to mind), but it's a bit more verbose, e.g:
Array.prototype.slice.call(arguments, 3);

No comments:

Post a Comment