Python object-oriented programming (OOP)
Object-oriented programming
Object-oriented programming also known as OOP is a programming paradigm that is based on objects having attributes (properties) and procedures (methods). The advantage of using Object-oriented programming(OOP) is that it helps in bundling the attributes and procedures into objects or modules. We can easily re-use and build upon these bundled objects/modules as per our needs.
Python OOP
Like many other programming languages (C++, Java, etc.), Python is an object oriented programming language (OOPL) from the very beginning (legacy stage). In Python OOP, we use classes.
A class in Python is a blueprint or a data structure of an object. It is just like a definition of something.
Creating our first class in Python
Creating a class in Python is as simple as:-
# python_oop.py
class Car:
pass
This class is just like a blueprint of a car from which we can create different cars. We call those different cars instances of the class Car.
# python_oop.py
class Car:
pass
car_1 = Car()
car_2 = Car()
print(car_1)
print(car_2)
# Output
<__main__.Car object at 0x1073c03c8>
<__main__.Car object at 0x1073c0518>
The car_1 and car_2 are two different instances/objects of our class Car.
Methods / Attributes in Python class
Every car has certain attributes like make, color, price, etc. which we need to have when we instantiate a car from our model. This can be done by defining them in one of our magic methods called ‘__init__‘ .
# python_oop.py
class Car:
def __init__(self, make, color, price):
self.make = make
self.color = color
self.price = price
The ‘__init__‘ method takes the instance as the first argument and by convention, we call the instance ‘self’.
Now, we can create various instances (cars) from this blueprint by passing the arguments specified in the __init__ method as under:-
car_1 = Car('Mercedes', 'Black', 100000)
car_2 = Car('Tesla', 'Blue', 60000)
print(car_1.make)
print(car_2.make)
print(car_2.price)
# Output
Mercedes
Tesla
60000
Note that the instance is passed automatically and we need not pass ‘self’ while creating the instances.
If we need to perform some kind of activity, we will add methods to our class. These methods/procedures allow us to add functionality to our class. Let us add a method to start the engine of the car within the class:-
class Car:
...
def start_engine(self):
return f'Vroom! {self.make} is ready to go!'
print(car_1.start_engine())
print(car_2.start_engine())
# Ouput
Vroom! Mercedes is ready to go!
Vroom! Tesla is ready to go!
The start_engine is a method and we must include () to execute it.
We can also run these methods directly from the class as under:-
# python_oop.py
print(Car.start_engine(car_1))
print(Car.start_engine(car_2))
# output
Vroom! Mercedes is ready to go!
Vroom! Tesla is ready to go!
Class variables in Python OOP class
The variables defined above i.e. make, color and price vary for different instances and are called instance variables. However, class variables are shared among all the instances of a class. Now, assume that all the automobile companies are running a promotion and giving an equal discount during the festive season. In that case, the discount amount will be a perfect candidate for the class variable.
# python_oop.py
class Car:
DISCOUNT = 0.10
...
def give_discount(self):
self.price = int(self.price * (1 - self.DISCOUNT))
car_1 = Car('Mercedes', 'Black', 100000)
print(car_1.price)
car_1.give_discount()
print(car_1.price)
# output
100000
90000
Since ‘DISCOUNT’ is a class variable, it is easy to change and we can also access the DISCOUNT for the class or an instance of the class as under:-
# python_oop.py
print(Car.DISCOUNT)
print(car_1.DISCOUNT)
# output
0.1
0.1
Here, we have not declared the ‘DISCOUNT’ for car_1 but when we print it, it first checks the instance for the variable and then fall back to the original class for the value of ‘DISCOUNT’. We can change the ‘DISCOUNT’ value for one instance and it will not change for the class or the other instances.
# python_oop.py
car_1.DISCOUNT = 0.15
print(Car.DISCOUNT)
print(car_1.DISCOUNT)
print(car_2.DISCOUNT)
# output
0.1
0.15
0.1
Regular methods, static methods and class methods in Python OOP class
Regular methods (as defined above) take the instance as a default argument for which ‘self’ is used as a general convention. But there could be use cases wherein we need to pass the class as the default argument; For such cases, class methods come handy. For example, we will create a class method, which will change the class variable ‘DISCOUNT’.
# python_oop.py
class Car:
DISCOUNT = 0.10
...
@classmethod
def set_discount(cls, discount):
cls.DISCOUNT = discount
car_1 = Car('Mercedes', 'Black', 100000)
car_2 = Car('Tesla', 'Blue', 60000)
Car.set_discount(.15)
print(Car.DISCOUNT)
print(car_1.DISCOUNT)
print(car_2.DISCOUNT)
# output
0.15
0.15
0.15
So, in the class method above, we have added a decorator @classmethod. The class method takes the class as a default argument, which we call ‘cls’ as a general convention(as ‘class’ is a reserved keyword). However, just like the regular method, we needn’t pass the class as an argument as the class method will automatically take it.
Class method as an alternative constructor
We can also use a class method as an alternative constructor for instantiating an object. For example, if we have the details of various cars as CSV where each row is as:
'kia,red,80000'
We can individually parse each row and then use it for creating the instances of the Car. However, if it is one of the common ways in which data is provided to our user, we can create an alternative constructor using a class method, which will take the comma-separated string as an input and create the instance of the Car.
# Individual parsing
car_string = 'Kia,Red,80000'
make, color, price = car_string.split(',')
car_3 = Car(make, color, int(price))
print(car_3.make)
# output
Kia
# Using class method as an alternative constructor
# python_oop.py
class Car:
...
@classmethod
def from_string(cls, car_string):
make, color, price = car_string.split(',')
return cls(make, color, int(price))
car_string = 'Kia,Red,80000'
car_3 = Car.from_string(car_string)
print(car_3.make)
# output
Kia
Static method in Python OOP class
As discussed above, regular methods take the instance as a default argument and the class methods take the class as a default argument. But there could be a method which has some logical connection with our class but need not take either of the class or instance as an argument. Such methods are called static methods. For example, few states in the US like Maryland, North Carolina, Iowa and South Dakota do not charge sales tax on certain cars. Let us create a method to find out will our car be taxed or not.
# python_oop.py
class Car:
...
@staticmethod
def is_taxed(state):
if state in ['Maryland', 'North Carolina', 'Iowa', 'South Dakota']:
return False
return True
print(Car.is_taxed('Ohio'))
# output
True
So, here we have used the decorator ‘@staticmethod’. In the ‘is_taxed()’ method above, we have not used the ‘cls’ or ‘self’, which clearly indicates that the said method should be static.
Inheritance in Python OOP classes
By using inheritance, we can inherit the attributes, methods, etc. of one class in another. The inheriting class is called the subclass, and the class from which it inherits is called the parent class. Both electric and gas cars have a make, color and price, but the electric cars have a range (how much will it run in a single charge) and gas cars have mileage. This makes them classic use cases of subclasses of the parent class Car.
Creating a subclass is as easy as under:-
# python_oop.py
class ElectricCar(Car):
pass
class GasCar(Car):
pass
By just passing Car as an argument to our ElectricCar() will make it inherits all the attribute of the Car():-
# python_oop.py
electric_car_1 = ElectricCar('Tesla', 'Blue', 60000)
gas_car_1 = GasCar('Mercedes', 'Black', 100000)
print(electric_car_1.make)
print(gas_car_1.make)
# output
Tesla
Mercedes
We will add attributes to our ElectricCar() and GasCar() classes.
# python_oop.py
...
class ElectricCar(Car):
def __init__(self, make, color, price, range):
super().__init__(make, color, price)
self.range = range
class GasCar(Car):
def __init__(self, make, color, price, mileage):
super().__init__(make, color, price)
self.mileage = mileage
electric_car_1 = ElectricCar('Tesla', 'Blue', 60000, 370)
gas_car_1 = GasCar('Mercedes', 'Black', 100000, 20)
print(electric_car_1.range)
print(gas_car_1.mileage)
# output
370
20
Passing ‘super().__init__()’ to the ‘__init__() ‘ method will automatically inherit the make, color and price from the parent class- Car().
We can use isinstance() to check whether an object is an instance of a specific class. Similarly, issubclass() will help us to determine whether a class is a subclass of a specific parent class.
# python_oop.py
...
print(isinstance(electric_car_1, ElectricCar))
print(isinstance(electric_car_1, Car))
print(isinstance(electric_car_1, GasCar))
print(issubclass(ElectricCar, Car))
print(issubclass(GasCar, Car))
# output
True
True
False
True
True
Magic/Dunder methods in Python OOP
Defining magic or dunder(double underscore) methods help us to change the built-in behaviour of the class. If you would have noticed, our class above already has a dunder method i.e. ‘__init__‘ method.
The other special methods that you should always use with your classes are dunder repr (‘__repr__‘) and dunder str (‘__str__‘).
The repr is the representation of an object is a piece of information for the developer and used for debugging, etc. However, str is a more user-friendly way of representing an object that is more readable and is meant for general users. In absence of the special repr and str methods, printing out an instance will give us this:-
# python_oop.py
print(car_1)
# output
<__main__.Car object at 0x10ad9b550>
The ‘repr’ method is the bare minimum you should have for a class because if you don’t have the special ‘str’ method, calling ‘str’ on an object will automatically fall to the ‘repr’ method. The repr’s output should be in the format which can be easily used to re-create the instance.
# python_oop.py
class Car:
...
def __repr__(self):
return f"Car('{self.make}','{self.color}',{self.price})"
car_1 = Car('Mercedes', 'Black', 100000)
print(repr(car_1))
print(car_1)
print(str(car_1))
# output
Car('Mercedes','Black',100000)
Car('Mercedes','Black',100000)
Car('Mercedes','Black',100000)
The output here is the same which was used to create car_1 object. Let us create the str method now. After creating the str method, the print(car_1) will automatically call the string method instead of repr method.
# python_oop.py
class Car:
...
def __str__(self):
return f'The {self.color} {self.make} costs {self.price}.'
car_1 = Car('Mercedes', 'Black', 100000)
print(repr(car_1))
print(car_1)
print(str(car_1))
# output
Car('Mercedes','Black',100000)
The Black Mercedes costs 100000.
The Black Mercedes costs 100000.
In some cases, we might need to run arithmetic operations like add or len etc. to our classes. It can be done by creating special methods for the same:-
# python_oop.py
class Car:
...
def __add__(self, other):
return self.price + other.price
car_1 = Car('Mercedes', 'Black', 100000)
car_2 = Car('Tesla', 'Blue', 60000)
print(car_1 + car_2)
# output
160000
Here, we have created an add function, which adds the price of the two cars. You can check out more functions from here.
Attributes with getter, setter and deleter using @property decorator
Using the @property decorator to our methods in the Python OOP class we can give it the functionality of getter, setter, and deleter. Have a look at the following example.
# python_oop.py
class Car:
DISCOUNT = 0.10
def __init__(self, make, color, price):
self.make = make
self.color = color
self.price = price
self.shortname = f'{make}-{color}'
car_1 = Car('Mercedes', 'Black', 100000)
car_2 = Car('Tesla', 'Blue', 60000)
print(car_1.shortname)
car_1.color = 'Red'
print(car_1.color)
print(car_1.shortname)
# output
Mercedes-Black
Red
Mercedes-Black
In the above example, we have added an attribute ‘shortname’ in our init method. But once the instance is created and we change its color, the shortname remains the same. This is because it has been set at the time of instantiating the object. To overcome this, we may come up with a method as under:-
# python_oop.py
class Car:
DISCOUNT = 0.10
def __init__(self, make, color, price):
self.make = make
self.color = color
self.price = price
# self.shortname = f'{make}-{color}'
def shortname(self):
return f'{self.make}-{self.color}'
car_1 = Car('Mercedes', 'Black', 100000)
car_2 = Car('Tesla', 'Blue', 60000)
print(car_1.shortname)
car_1.color = 'Red'
print(car_1.color)
print(car_1.shortname)
The problem here is that, when we created a method for the shortname, it can not be called as an attribute and we will have to add the parenthesis (shortname()). Otherwise, the output will be as under:-
<bound method Car.shortname of <__main__.Car object at 0x10180d438>>
Red
<bound method Car.shortname of <__main__.Car object at 0x10180d438>>
But adding () at the end of shortname will be cumbersome as the end-user will have to search for all the calls to the shortname attribute and change it to the method. Or we can add the property decorator, which will allow us to call the shortname method just as an attribute and hence preserve the rest of our code.
# python_oop.py
class Car:
...
@property
def shortname(self):
return f'{self.make}-{self.color}'
car_1 = Car('Mercedes', 'Black', 100000)
print(car_1.shortname)
car_1.color = 'Red'
print(car_1.color)
print(car_1.shortname)
# output
Mercedes-Black
Red
Mercedes-Red
So, by using the property attribute as a getter, we could change the shortname on changing the car’s color and also preserve our code.
Let us assume that we want to change the make and color of our car instance by doing this :-
car_1.shortname = 'Mercedes Copper'
Currently, you can not do that and you will get the following AttributeError:-
Traceback (most recent call last):
File "/Users/uditvashisht/Desktop/coding/code_snippets/python_oop/python_oop.py", line 113, in <module>
car_1.shortname = 'Mercedes Copper'
AttributeError: can't set attribute
But you can use setters to make it work:-
# python_oop.py
class Car:
...
@property
def shortname(self):
return f'{self.make}-{self.color[0].upper()}'
@shortname.setter
def shortname(self, name):
make, color = name.split(' ')
self.make = make
self.color = color
car_1 = Car('Mercedes', 'Black', 100000)
car_1.shortname = 'Mercedes Copper'
print(car_1.color)
# output
Copper
Here we have created a new method with the same name ‘shortname’ and add a decorator @shortname.setter to it.
Similarly, we can create a deleter to delete certain attributes of a class instance.
# python_oop.py
class Car:
...
@shortname.deleter
def shortname(self):
self.make = None
self.color = None
car_1 = Car('Mercedes', 'Black', 100000)
del(car_1.shortname)
print(car_1.color)
# output
None
I think this covers most of the Object-Oriented Programming in Python. If you think there is something more to add, feel free to comment.
If you liked our tutorial, there are various ways to support us, the easiest is to share this post. You can also follow us on facebook, twitter and youtube.
If you want to support our work. You can do it using Patreon.