Let's start the discussion of inheritance by inheriting from our Device class, we will build a second class called Router that will inherit from Device class. This makes logical sense because of the "is-a" relationship: a router is a device.
class Device:
def __init__(self, ip, vendor):
self.ip = ip
self.vendor = vendor
def __str__(self):
return f"<{type(self).__name__}: {self.vendor}, IP: {self.ip}>"
def __eq__(self, other):
return self.ip == other.ip
class Router(Device):
pass
if __name__ == "__main__":
R1 = Router(ip="192.168.10.1", vendor="Cisco")
print(R1)
print(f"R1's IP is {R1.ip}")
print(f"R1 is a {R1.vendor} {type(R1).__name__}")
print(f"Class Hierarchy {Router.__mro__}")
We have created a class called Router that inherits from Device class, unlike Java, Python allows class inheritance from multiple super classes but let's focus on single class inheritance for now.
By inheriting from Device class, the Router class has all its methods and functions inherited as well, so even though we haven't defined anything in Router class yet but we can still use Device class's initializer method (__init__) and can initialise our object of Router class by passing ip and vendor
Output
─$ python3 oop_basics.py
<Router: Cisco, IP: 192.168.10.1>
R1's IP is 192.168.10.1
R1 is a Cisco Router
Class Hierarchy (<class '__main__.Router'>, <class '__main__.Device'>, <class 'object'>)
As expected, printing Router class's object R1 returned us the value as per __str__ method of Device class but it right shows class name of Router instead of Device as it is called on an object of Router class.
Correct printing of R1's ip and vendor confirmed we still have access to the variables defined in Device class.
One new thing we have done here is print of __mro__ dunder method on Router class and it's output tells us Router class inherits from Device class and Device class inherits from Object class. When creating Device class we didn't specify that it will be inheriting from Object class but it still does that points to the fact that all classes in Python inherit from Object class by default. Object class is at the bottom of the class hierarchy and that is where all these dunder methods are defined, that's when we define a class we automatically get that by virtue of inheritance.
Keep in mind this obvious fact that all methods and properties of parent class are available to child class but no new method or property of child class is available in parent class.
Parent class is also called a super class in Python so if we want to refer to a method in our parent class from child class we will append super before the method name. Let's expand our Router class.
Private & Protected Variables
Let's take a step back from Inheritance and discuss private and protected variables and methods as we need to understand what happens to them when we inherit from a class. Unlike Java, Python don't enforce private or protected variables/methods and leave it to the user of class to honour it or not. In Python, any variable is considered protected if it's name starts with an underscore _ and a variable is considered private if it starts with double underscore __. But it is only advisory, they are still perfectly accessible.
Ideally, if you use protected variables then use getters and setters to pull their value and set their value, even though Python doesn't enforce that.
Enough on private variables and methods, let's get back to our discussion of inheritance, we will revisit them in another post.
Extend Router class
By now we understand whatever is in parent class will be accessible from child class. Let's extend our Router class and add some properties and methods to it.
class Device:
def __init__(self, ip, vendor):
self.ip = ip
self.vendor = vendor
def __str__(self):
return f"<{type(self).__name__}: {self.vendor}, IP: {self.ip}>"
def __eq__(self, other):
return self.ip == other.ip
class Router(Device):
def __init__(self, ip, vendor, routing_protocols=None):
super().__init__(ip, vendor)
if routing_protocols is None:
self.routing_protocols = []
self.routing_protocols = routing_protocols
self.routing_table = {}
if __name__ == "__main__":
R1 = Router(ip="192.168.10.1", vendor="Cisco")
print(R1)
print(f"R1's IP is {R1.ip}")
print(f"R1 is a {R1.vendor} {type(R1).__name__}")
print(f"Class Hierarchy {Router.__mro__}")
A few things to note in our updated Router class.
super().__init__(ip, vendor) - we are calling initializer from super class (Device) and passing it ip and vendor as these are two parameters initializer of our Device class expects. If we need to extend the initializer method of super class then in the first line of our extended initializer we call initializer of super class and pass it required arguments.
- When we define a function or a method that expect some variables those variables are called parameters.
- When we call that function and pass some values for those variables these values are called arguments.
if routing_protocols is None:
routing_protocols = []
This if statement assigns an empty list to routing_protocols if user didn't provide any value for it, otherwise in next line a user provided value is assigned to the routing_protocols. We could directly assign an empty list to routing_protocols as default value but that can be dangerous as lists are passed by reference and can be changed outside our class.
And finally, notice we can directly add object variables as well within our initializer, like we did in case of routing_table variable in our class. We will be extracting routing table from the router when we connect to it so we expect user to pass the routing at object initialization.
- In Python, there are two stages of an object creation, `__new__` is called when an object is created and memory allocation is done. `__init__` is called when data is initialised in the object. We usually set values in initializer (`__init__`) and almost never touch constructor (`__new__`) in Python.
- In other languages like Java and C++ a constructor does both jobs of object creation and initialisation so we set values in constructor in Java or C++.
Practice
- Create a new class
Switchand inherit it fromDeviceclass - For
Switchclass object variables ofswitch_typewith default value of 'Layer2', vlans, and mac_address_table. vlans and mac_address_table will be calculated after object creation so just assign empty dicts to both object variables for now.
What's Next
In the next post, we will discuss class properties and methods and we will create some class methods to connect to routers/switches and pull information, parse it and then save in the objects we defined in this class.
Thanks for stopping by, keep visiting and we will build a full class hierarchy for our network and populate it through network discovery while learning object oriented programming principles.