Python is an amazing programming language that supports both the functional programming paradigm and object-oriented programming paradigm. A Python programmer, be it a software developer or a machine learning engineer or something else, is expected to be familiar with object-oriented programming. Python‘s object-oriented programming system supports all the four fundamental features of a general OOPS framework: encapsulation, abstraction, inheritance and polymorphism. We will have a quick look and hands-on practice on these features in this tutorial.

What is a Class and an Object?

Python, similar to any other object-oriented language, enables creating objects by defining classes. The most common data types of Python, such as strings, lists, dictionaries, etc., are in-built Python classes. A class is a bundle of instance variables and related methods meant for defining a type of object. A class can be viewed as a blueprint or a template of the objects. Variables of a class are usually termed as attributes. An object is an instance of a class with a specific set of attributes. Thus, one can create as many objects as needed from the same class. Coding examples in the sequel will give a better understanding of a class and objects.

Let’s define a class named Poetry for a bookseller’s sales software.

 class Poetry():
   def __init__(self, title, poems_count, author, price):
     self.title = title
     self.poems_count = poems_count
     self.author = author
     self.price = price 

The class Poetry is initialized using the __init__ special method with attributes such as title, poems count, author and price. It is a custom that in-built Python class names are in lower case and user-defined classes are in Camel case or Snake case with the first letter capitalized.

This class can be instantiated to any number of objects. Here, in the below example codes, we are going to instantiate three poetries.

 poem_1 = Poetry('Leaves of Grass', 383, 'Walt Whitman', 600)
 poem_2 = Poetry('Milk and Honey', 179, 'Rupi Kaur', 320)
 poem_3 = Poetry('Life on Mars', 33, 'Tracy K. Smith', 100) 

poem_1, poem_2 and poem_3 are distinct objects; they are three distinct instances of the class Poetry. The term ‘self’ in the attributes refers to the corresponding instances (objects).

 print(poem_1)
 print(poem_2)
 print(poem_3) 

Output:

On printing the objects, they express their class and memory location. We cannot expect them to give details about the attributes, i.e., the title, author name, etc. However, this can be performed by using a special method, called ‘__repr__’. A special method in Python is a defined function that begins and ends with two underscores, and it is automatically called under specific circumstances.

class Poetry():
   def __init__(self, title, poems_count, author, price):
     self.title = title
     self.poems_count = poems_count
     self.author = author
     self.price = price
   def __repr__(self):
     return f'Poetry: {self.title} by {self.author}, price {self.price}'
# objects instantiation
 poem_1 = Poetry('Leaves of Grass', 383, 'Walt Whitman', 600)
 poem_2 = Poetry('Milk and Honey', 179, 'Rupi Kaur', 320)
 poem_3 = Poetry('Life on Mars', 33, 'Tracy K. Smith', 100)
 # printing the objects
 print(poem_1)
 print(poem_2)
 print(poem_3) 

Output:

object description

Know more about Pythons’s special methods here.

Encapsulation

Encapsulation is the process of making certain attributes inaccessible to their clients and can only be accessed through certain methods. The inaccessible attributes are called private attributes, and the process of making certain attributes private is called information hiding. Private attributes begin with two underscores. 

In the above Poetry class, we introduce a private attribute named ‘__discount’.

class Poetry():
   def __init__(self, title, poems_count, author, price):
     self.title = title
     self.poems_count = poems_count
     self.author = author
     self.price = price
     self.__discount = 0.20 
   def __repr__(self):
     return f'Poetry: {self.title} by {self.author}, price {self.price}'
 poem_1 = Poetry('Leaves of Grass', 383, 'Walt Whitman', 600)
 print(poem_1.author)
 print(poem_1.title)
 print(poem_1.price)
 print(poem_1.__discount) 

Output:

Object AttributeError

Private attributes are accessed through methods called getter and setter. In the following code example, we make the price attribute private; we assign the discount attribute through a setter method and read the price attribute through a getter method.

 class Poetry():
   def __init__(self, title, poems_count, author, price):
     self.title = title
     self.poems_count = poems_count
     self.author = author
     self.__price = price
     self.__discount = None

   def set_discount(self, value):
     self.__discount = value

   def get_price(self):
     if self.__discount is None:
       return self.__price
     else:
       return self.__price * (1 - self.__discount)

   def __repr__(self):
     return f'Poetry: {self.title} by {self.author}, price {self.get_price()}' 

Let’s create two objects of the same Poetry, one for retail purchase and another for bulk purchase. We assign the bulk purchase object with a discount of 30%.

 retail_purchase = Poetry('Leaves of Grass', 383, 'Walt Whitman', 600)
 bulk_purchase = Poetry('Leaves of Grass', 383, 'Walt Whitman', 600)
 # assign 30% discount to bulk purchase alone
 bulk_purchase.set_discount(0.30)

 print(retail_purchase.get_price())
 print(bulk_purchase.get_price())
 print(retail_purchase)
 print(bulk_purchase) 

Output:

Inheritance

Inheritance is considered the most important feature in an OOPS. Inheritance is the ability of a class to inherit methods and/or attributes of another class. The inheriting class is called the subclass or the child class. The class from which methods and/or attributes are inherited is called the superclass or the parent class.

Our bookseller’s sales software is now appended with two more classes, a Play class and a Novel class. We can understand that whether a book comes under a Poetry or Play or Novel category, it might have some common attributes such as title and author, and common attributes such as get_price() and set_discount(). It is a waste of time, effort and memory to rewrite all those codes again for each new class.

Therefore, we create a superclass, Book(), from which other subclasses inherit common attributes and methods.

 class Book():
   def __init__(self, title, author, price):
     self.title = title
     self.author = author
     self.__price = price
     self.__discount = None

   def set_discount(self, value):
     self.__discount = value

   def get_price(self):
     if self.__discount is None:
       return self.__price
     else:
       return self.__price * (1 - self.__discount) 

Now, the already introduced Poetry class can be modified as below to inherit the Book class.

 class Poetry(Book):
   def __init__(self, title, poems_count, author, price):
     super().__init__(title, author, price)
     self.poems_count = poems_count

   def __repr__(self):
     return f'Poetry: {self.title} by {self.author}, price {self.get_price()}' 

To realize how inheritance works, we can instantiate a Poetry object, set a discount and get its price.

 poem_1 = Poetry('Leaves of Grass', 383, 'Walt Whitman', 600)
 print(poem_1)
 poem_1.set_discount(0.15)
 print(poem_1) 

Output:

Similar to Poetry class, two more subclasses are created to inherit from the Book class.

 class Play(Book):
   def __init__(self, title, genre, author, price):
     super().__init__(title, author, price)
     self.genre = genre

   def __repr__(self):
     return f'{self.genre} Play: {self.title} by {self.author}, price {self.get_price()}'

 class Novel(Book):
   def __init__(self, title, pages, author, price):
     super().__init__(title, author, price)
     self.pages = pages

   def __repr__(self):
     return f'Novel: {self.title} by {self.author}, price {self.get_price()}' 

And we do a check to visualize how it works.

 play_1 = Play('Romeo and Juliet', 'Tragedy', 'William Shakespeare', 160)
 novel_1 = Novel('To kill a Mockingbird', 281, 'Harper Lee', 310)
 print(play_1)
 print(novel_1) 

Output:

Inherit a class

Polymorphism

The word ‘polymorphism’ is derived from the Greek language, meaning ‘something that takes different forms’. Polymorphism is a subclass’s ability to customize a method as per need that is already present in its superclass. In other words, a subclass may either use a method in its superclass as such or modify it suitably whenever required. 

 class Book():
   def __init__(self, title, author, price):
     self.title = title
     self.author = author
     self.__price = price
     self.__discount = None

   def set_discount(self, value):
     self.__discount = value

   def get_price(self):
     if self.__discount is None:
       return self.__price
     else:
       return self.__price * (1 - self.__discount)

   def __repr__(self):
     return f'{self.title} by {self.author}, price {self.get_price()}'

 class Poetry(Book):
   def __init__(self, title, poems_count, author, price):
     super().__init__(title, author, price)
     self.poems_count = poems_count

 class Play(Book):
   def __init__(self, title, genre, author, price):
     super().__init__(title, author, price)
     self.genre = genre

   def __repr__(self):
     return f'{self.genre} Play: {self.title} by {self.author}, price {self.get_price()}'

 class Novel(Book):
   def __init__(self, title, pages, author, price):
     super().__init__(title, author, price)
     self.pages = pages 

It can be seen that the Book superclass has a special method __repr__. Subclasses Poetry and Novel can use this method as such, so that whenever an object is printed, this method will be invoked. On the other hand, in the above example code, the Play subclass is defined with its own __repr__ special method. By polymorphism, the Play subclass will invoke its own method by suppressing the same method available in its superclass.

 poem_2 = Poetry('Milk and Honey', 179, 'Rupi Kaur', 320)
 play_2 = Play('An Ideal Husband', 'Comedy', 'Oscar Wilde', 240)
 novel_2 = Novel('The Alchemist', 161, 'Paulo Coelho', 180)
 
 print(poem_2)
 print(play_2)
 print(novel_2) 

 Output:

polymorphism of a class

Abstraction

Python does not have a direct support for abstraction. However, abstraction is enabled by calling a magic method. If a method in a superclass is declared to be an abstract method, subclasses that inherit from the superclass must have their own versions of the said method. An abstract method in a superclass will never be invoked by its subclasses. But, the abstraction helps maintain a certain common structure in all of the subclasses. 

In our bookseller sales software example, we have defined __repr__ methods for each of the subclasses under the Inheritance subheading; we have defined a common __repr__ method in a superclass that a subclass may invoke if it fails to have its own method under the Polymorphism subheading. Here, in this part, the superclass will have an abstract __repr__ method forcing every subclass to compulsorily have their own __repr__ method.

 from abc import ABC, abstractmethod
 class Book(ABC):
   def __init__(self, title, author, price):
     self.title = title
     self.author = author
     self.__price = price
     self.__discount = None

   def set_discount(self, value):
     self.__discount = value

   def get_price(self):
     if self.__discount is None:
       return self.__price
     else:
       return self.__price * (1 - self.__discount)

   @abstractmethod
   def __repr__(self):
     return f'{self.title} by {self.author}, price {self.get_price()}'

 class Poetry(Book):
   def __init__(self, title, poems_count, author, price):
     super().__init__(title, author, price)
     self.poems_count = poems_count

 class Play(Book):
   def __init__(self, title, genre, author, price):
     super().__init__(title, author, price)
     self.genre = genre

   def __repr__(self):
     return f'{self.genre} Play: {self.title} by {self.author}, price {self.get_price()}' 

We intentionally miss here to define a __repr__ method separately for Poetry subclass. 

 play_3 = Play('Death of a Salesman', 'Tragedy', 'Arthur Miller', 240)
 poem_3 = Poetry('Life on Mars', 33, 'Tracy K. Smith', 100) 

Output:

Abstraction class

We get a TypeError for the Poetry object!

The correct implementation of an abstract class with an abstract method is as below:

 from abc import ABC, abstractmethod

 class Book(ABC):
   def __init__(self, title, author, price):
     self.title = title
     self.author = author
     self.__price = price
     self.__discount = None

   def set_discount(self, value):
     self.__discount = value

   def get_price(self):
     if self.__discount is None:
       return self.__price
     else:
       return self.__price * (1 - self.__discount)

   @abstractmethod
   def __repr__(self):
     return f'{self.title} by {self.author}, price {self.get_price()}'

 class Poetry(Book):
   def __init__(self, title, poems_count, author, price):
     super().__init__(title, author, price)
     self.poems_count = poems_count

   def __repr__(self):
     return f'Poetry: {self.title} by {self.author}, {self.poems_count} poems, price {self.get_price()}'

 class Play(Book):
   def __init__(self, title, genre, author, price):
     super().__init__(title, author, price)
     self.genre = genre

   def __repr__(self):
     return f'Play: {self.title} by {self.author}, {self.genre} genre, price {self.get_price()}'

 class Novel(Book):
   def __init__(self, title, pages, author, price):
     super().__init__(title, author, price)
     self.pages = pages

   def __repr__(self):
     return f'Novel: {self.title} by {self.author}, {self.pages} pages, price {self.get_price()}' 

Some example object instantiations can be:

 poem_3 = Poetry('Life on Mars', 33, 'Tracy K. Smith', 100)
 play_3 = Play('Death of a Salesman', 'Tragedy', 'Arthur Miller', 240)
 novel_3 = Novel('Peril at End House', 270, 'Agatha Christie', 210)
 
 print(poem_3)
 print(play_3)
 print(novel_3) 

Output: 

This Colab Notebook has the above code implementations.

Learn more from the official documentation.

The post Object-Oriented Programming with Python appeared first on Analytics India Magazine.