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:
- Official python ‘Classes’ tutorial
- Data model section of the official python ‘Language Reference’
- “Supercharge your classes with Python super() article on realpython.org” - a really good read
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 ofC.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).