Python3 OOP gotchas

This is not a tutorial, but rather a personal document that I write for myself in order to cement the knowledge and explain “tricky” concepts and gotchas when it comes to OOP and classes in python3

Useful resources:

How does self actually work?

Consider the following class definition:

 1class Dog:
 2    def __init__(self, name):
 3        self.name = name
 4
 5    def speak(self, age):
 6        return f"My name is {self.name} and I am {age} years old."
 7
 8jake = Dog("Jake")
 9print(jake.speak(3))
10
11"My name is Jake and I am 3 years old."

When we call jake.speak, the self arguement is “automatically” passed into the method because the method belongs to an already instantiated instance of a class.

We can achieve the same effect by instantiating jake, then calling Dog.speak directly while passing the value of the self argument manually, like so:

1jake = Dog("jake")
2print(Dog.speak(jake, 3))
3
4"My name is Jake and I am 3 years old."

The same is true for the __init__ method, which is a method like any other, except that it is called automatically when instantiating new instances of the class.

1import types
2
3jake = types.SimpleNamespace()
4Dog.__init__(jake, "Jake")
5
6print(jake.name)
7
8"Jake"

Of course this is undesirable, as now we cannot access other methods defined in the class (e.g. speak).

super() and Method Resolution Order (MRO)

Method Resolution Order (MRO) is the order in which method lookup is performed by the interpreter for an instance of a given class.

Defined classes have an __mro__ attribute that results in a tuple describing the MRO.

Instances don’t have this method defined, but this can very easily be circumvented using the special instance.__class__ attribute, which returns the reference of the class that the instance belongs to:

1print(instance.__class__.__mro__)

Examine the following code:

 1class A:
 2    def mymethod(self):
 3        return "mymethod of class A"
 4
 5class B(A):
 6    def mymethod(self):
 7        return "mymethod of class B"
 8
 9class C(B):
10    def mymethod(self):
11        return "mymethod of class C"
12    
13instance = C()
14print(f"C.__mro__ = {C.__mro__}")
15print(instance.mymethod())

This then logs:

1C.__mro__ = (<class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>)
2mymethod of class C

This means that any method lookups done on instances of class C will first be done on methods defined class C, then class B, then class A, and then in the “base” object class, stopping at the first class in which the desired method is found, because every class in the “chain” inherits the next one (C inherits B, which inherits A which by default inherits object).

In our case, the mymethod method was in fact defined in the first class listed in the __mro__ tuple, so naturally, it is C.mymethod that gets called in the end.

If we remove the mymethod method from the C class, the MRO will stay the same, however since mymethod doesn’t exist inside of the C class anymore, it will instead “come” from the next one in the MRO chain that it is defined in, in this case the B class.

1class C(B):
2    pass
3
4instance = C()
5print(f"C.__mro__ = {C.__mro__}")
6print(instance.mymethod())

this logs:

1C.__mro__ = (<class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>)
2mymethod of class B

super() explained

The official super() builtin documentation explains its use and functionality pretty well, but to explain it using an example: let’s say that we wanted the mymethod method in the C class to return the same string returned by the mymethod method of the A class, with an additional ! appended at the end.

Obviously that is a simplified example, in the real world A.mymethod could execute some long, complicated logic that we wish to re-implement (alongside some other logic) inside of C.mymethod.

Inside of the C class we can accomplish this by calling super() which creates a proxy object that delegeates all method calls to the appropriate class:

1class C(B):
2    def mymethod(self):
3        return super(B, self).mymethod() + "!!!"

The output will therefore be: mymethod of class A!!!.

super() is more useful when you provide no arguments to it however, as it automatically goes up the hierarchy and provides you with the first class that defines the appropriate method (or attribute, as super() works for attribute lookups as well).