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.