30 Days Of Python πŸ‘¨β€πŸ’» - Day 10 - OOP Missing Pieces

This article is a part of a 30 day Python challenge series. You can find the links to all the previous posts of this series here:
I almost explored all the key OOP concepts yesterday. Today I went through the remaining bits and pieces of Object Oriented Programming concepts and their implementation in Python. Along with that, I have tried to include some practical code exercises that involve the overall usage of OOP concepts in Python to recall all the concepts from the mental model that has been developed.
 

super()

 
super is a reserved word in Python (was introduced in Python v 2.2) which comes into action during inheritance. When a subclass or child class which inherits from a parent class and needs to call a method of the parent class, it uses super. I know this sounds quite confusing. So here’s an example.
 
Without using super
  1. class Employee:  
  2.   def __init__(self, name):  
  3.     self.name = name  
  4.     print(f'{self.name} is an employee')  
  5.   
  6. class Manager(Employee):  
  7.   def __init__(self, department, name):  
  8.     self.department = department  
  9.     self.name = name  
  10.     Employee.__init__(self, name)  
  11.     print(f'Manager, {self.department} department')  
  12.   
  13. staff_1 = Manager('HR''Andy')  
  14. # Andy is an employee  
  15. # Manager, HR department  
Here, the __init__constructor method of the parent class is called by explicitly using the parent class name and then the self object is passed as the first parameter.
 
Using super (Compact syntax - No need to pass self)
  1. class Employee:  
  2.   def __init__(self, name):  
  3.     self.name = name  
  4.     print(f'{self.name} is an employee')  
  5.   
  6. class Manager(Employee):  
  7.   def __init__(self, department, name):  
  8.     self.department = department  
  9.     self.name = name  
  10.     super().__init__(name)  
  11.     print(f'Manager, {self.department} department')  
  12.   
  13. staff_1 = Manager('HR''Andy')  
  14. # Andy is an employee  
  15. # Manager, HR department  
Just like the constructor method shown in the above code, any method of the parent class can be called inside the child class using super().
 
In JavaScript, the syntax is more compact where super is called like super (parameter). But I like the Python syntax as well. It is more explicit about calling the __init__ method using super.
 

Introspection

 
Python is able to evaluate the type of an object (everything in Python is an object) at runtime. It means the interpreter is able to understand what are the properties and methods of the object and their accessibility at runtime dynamically. This is called introspection.
 
Python provides a built-in function dir to introspect an object.
  1. class Developer:  
  2.   def __init__(self, name, language):  
  3.     self.name = name  
  4.     self.language = language  
  5.     
  6.   def introduce(self):  
  7.     print(f'Hi! I am {self.name}. I code in {self.language}')  
  8.   
  9. dev = Developer('Matt''Python')  
  10.   
  11. print(dir(dev)) # Try this in any Python REPL  

Dunder Methods

 
In Python, classes can be made more powerful by defining some magical methods called dunder methods. Dunder is a short name for double-under. These methods are prefixed and suffixed by double underscores __. These special methods are predefined in Python for specific use cases. For example, we are able to access the built-in function because it is defined as a special dunder method __len__.
 
When creating a class, these dunder methods can be used to simulate the behaviour of built-in types.
  1. class Sentence:  
  2.   words = []  
  3.     
  4.   def add_word(self, word):  
  5.     self.words.append(word)  
  6.   
  7.   def __len__(self):  
  8.     return len(self.words)  
  9.   
  10. new_sentence = Sentence()  
  11. new_sentence.add_word('Hello')  
  12. new_sentence.add_word('World')  
  13. print(len(new_sentence))  
I modified the Sentence class so that we can use the built-in method len which is not available by default to implement custom logic. Dunder methods seem quite handy!
 

Multiple Inheritance

 
It is possible for a class to inherit properties and methods from multiple classes via Multiple Inheritance. It is a powerful concept but has its caveats as well. In comparison with JavaScript universe, Multiple Inheritance is not supported there.
  1. class Batsman:  
  2.   def swing_bat(self):  
  3.     return 'What a shot!'  
  4.   
  5. class Bowler:  
  6.   def bowl_bouncer(self):  
  7.     return 'What a bouncer!'  
  8.   
  9. class AllRounder(Batsman, Bowler):  
  10.   pass  
  11.   
  12. player = AllRounder()  
  13.   
  14. print(player.bowl_bouncer()) # What a shot!  
  15. print(player.swing_bat()) # What a bouncer!  
It can get a bit complicated when parent classes have constructor methods that require initialization. In the child class, all the inherited class constructor methods need to be initialized.
  1. class Batsman:  
  2.   def __init__(self, hitting_power):  
  3.     self.hitting_power = hitting_power  
  4.   
  5.   def swing_bat(self):  
  6.     return f'Shot with power {self.hitting_power}'  
  7.   
  8. class Bowler:  
  9.   def __init__(self, delivery_speed):  
  10.     self.delivery_speed = delivery_speed  
  11.   
  12.   def bowl_bouncer(self):  
  13.     return f'Bowled with speed of {self.delivery_speed} kmph'  
  14.   
  15. class AllRounder(Batsman, Bowler):  
  16.   def __init__(self, hitting_power, delivery_speed):  
  17.     Batsman.__init__(self, hitting_power)  
  18.     Bowler.__init__(self, delivery_speed)  
  19.   
  20. player = AllRounder(9080)  
  21. print(player.swing_bat())  
  22. print(player.bowl_bouncer())  

Method Resolution Order

 
Method Resolution Order or mro in short, is the order in which properties and methods are inherited in Python.
 
When inheriting from multiple classes, the properties and methods are inherited by the child class in a specific hierarchy. The underlying algorithm that implements this in Python uses Depth-first search algorithm.
  1. class Employee:  
  2.   secret_code = 'secret'  
  3.   
  4. class Manager(Employee):  
  5.   secret_code = 'm123'  
  6.   
  7. class Accountant(Employee):  
  8.   secret_code = 'a123'  
  9.   
  10. class Owner(Manager, Accountant):  
  11.   pass  
  12.   
  13. person = Owner()  
  14. print(person.secret_code) # m123  
To know the order of inheritance, Python provides a method mro that can be called on the object to view the hierarchy of inheritance
  1. print(Owner.mro()) # try in a python console with above code to see the result  
Multiple inheritances can be difficult to understand so this pattern is not commonly used in practice. This is what I read in several articles.
 
That’s all for today!
 
We are finally done with the Object-Oriented Programming concepts in Python. The goal is to implement these principles when I start building real Python projects once this challenge is over.
 
I hope I was able to cover all the key Object Oriented Programming concepts in Python and share it without sounding very complicated.
 
Video I am currently watching
 
Oral History of the Python Founder
 
Tomorrow I will plunge into the territory of functional programming in Python. It’s gonna be quite exciting for sure. Although Python, in general, is a Procedural Language and is popular for its Object-Oriented concepts, I will explore how functional style programming concepts can be implemented in Python for the rest of this week.
 
Have a great one!