Object-oriented programming (OOP) concepts in Python with practical examples.

Object-oriented programming (OOP) concepts in Python with practical examples.

Object-oriented programming (OOP) with Python programming language.

Introduction

What is OOP?

Object-oriented programming refers to a programming practice that represents real world entities like an employee or a product, and their attributes as objects. This ensures that the data and attributes associated with these entities are easily manipulated, accessed and optimized.

It also involves the breakdown of an app, software or coding process into smaller re-usable parts. Each part is then treated as a separate object. OOP is useful to write cleaner and reusable code.

OOP is useful when building a software with a lot of parts to it. For example, building a brick game with Python turtle library. A brick game has a ball, multiple bricks, a paddle and a scoreboard. The best approach is to divide the project into different parts, whereby the ball, bricks and paddle are treated as separate objects.

The re-usability of code in OOP comes in handy when you need to build another project that requires a paddle or a ball-bouncing logic. You can easily go back to your brick game project, get the ball object and its code with little or no customization for your new project.

Tutorial Objective

This tutorial is for anyone who is new to OOP principles in Python. It is a beginner-friendly guide that will help you understand the concept of OOP in Python and how to use it in your project. In this tutorial, you will learn the following:

  1. Class keyword in Python

  2. Objects and instances

  3. Attributes and methods

  4. Inheritances in object-oriented programming

  5. Encapsulation in object-oriented programming

  6. Polymorphism in object-oriented programming

Requirements

  1. Python basics: You need a basic knowledge of Python programming concepts such as loops, variables, functions and data types.

  2. Code Interpreter: A code interpreter or IDE like Pycharm, VS Code, etc. with Python3+ installed on your computer.

OOP in Python

The class keyword.

The class keyword is used to imitate the OOP concept in Python. It is used to declare a Python object that represents a real word entity. This object is often referred to as a Python class.

The Python class serves as a blueprint for creating multiple variations of the entity it represents. Each of these variations are known as instances, and sometimes referred to as objects.

A Python class can be used to create custom data types, and organize code into a more structured and reusable form.

Example of a class

class Product:
    pass

As seen in the code above, the class keyword is followed by the class name - Product. Generally, class names are written in camel case with examples like SoftwareDeveloper, TechnicalWriter, etc. However, your code will still run and function correctly if you do otherwise.

Python objects and instances

An object in Python refers to anything that has an attribute and a method or function. A string is an object in Python. For example, the word good is a string and is also an object because it has attributes and methods such as: .capitalize(), .upper(), .lower(), etc. for different operations. Below are other examples


# Converts the string to uppercase
print(variable.upper())
>>> GOOD


# converts the first character to uppercase it
print(variable.capitalize())
>>> Good

Instance, on the other hand, refers to an occurrence of a Python class. An instance uses the attributes and methods declared within a class. The example below shows you how to create an instance from a class

class Product:
    pass

bag = Product()

In the example above, the bag variable is an instance of the Product class.

Attributes and Methods

What are attributes?

Attributes refer to the features or unique qualities of an instance or a class.

Types of Attributes

There are two types of attributes in Python OOP. They include the following:

Class attributes

This refers to the general attributes that are set on a class. They are global attributes that can be accessed by each instance of the class. They are used in cases where all instances of a class have an attribute in common.

The Product class can have attributes such as form (solid or liquid), type (digital or physical), size (large or small). These kinds of attributes are called class attributes.

class Product:
    product_type = "physical"

bag = Product()
chair = Product()

# Using the bag instance
print(bag.product_type)
>>> "physical"

# Using the chair instance
print(chair.product_type)
>>> "physical"

# Using the Product class itself
print(Product.product_type)
>>> "physical"

In the code snippet above, the product_type attribute is added to the Product class. The product_type remains the same for every instance of the Product class that is created.

The Product class can also access the attribute directly without an instance. Attributes are accessed using any of these format — <instance_name>.<attribute> or <class_name>.<attribute>

Instance attributes

This refers to the unique attributes of each instance of a class. These attributes are specific to each instance and can be modified only by the instance itself. Below is an example of how instance attributes are used:

class Product:

    def __init__(self, colour, quantity):
        self.colour = colour
        self.quantity = quantity

radio = Product("black", 3)
bag = Product("blue", 10)
print(radio.colour)
>>> "black"

print(radio.quantity)
>>> 3

print(bag.colour)
>>> "blue" 

print(bag.quantity)
>>> 10

In the snippet above, the two instances of the Product class — radio and bag have a colour and quantity attribute. Each instance has a unique value for their attribute.

The __init__() function is used to create an instance attribute within a class. This function runs immediately after an instance is created. This helps to declare the instance attributes for quick access. It also accepts positional and keyword arguments which act as the unique attribute of each instance.

The self argument refers to an instance of the class. It is a compulsory argument that must be included. An instance attribute is set by using the self.<attribute> syntax.

What is a method?

A method is a function or action that can be called by a class or an instance. Methods are defined within a class and operate on the data or attributes of that class. They allow you to perform actions specific to an instance of a class or the class itself.

Types of methods

In Python, there are four types of methods within a class. They include:

  1. Class methods

  2. Instance methods

  3. Static methods

  4. Special methods (Dunder methods).

This guide will only cover the first two types of methods (class and instance) while we the rest are briefly discussed.

Class methods

This type of method operates on the class itself rather than on each instance. Like a class attribute, it is also a global method. They are used for operations that are common to each instance of a class. Below is an example of a class method:

class Trader:

    @classmethod
    def sell(cls):
        print("A Trader sold an item")

trader = Trader()
trader.sell()
>>> "A Trader sold an item"


Trader.sell()
>>> "A Trader sold an item"

In the example above, the Trader class has a method named sell() — considering that all traders sell an item.

The @classmethod decorator is used to create a class method as seen in the code snippet above. A class method takes cls as a compulsory argument which refers to the class itself. This method can be called by either an instance of the class or the Trader class itself.

Instance methods

This refers to methods that are specific and unique to each instance of a class. Unlike the class methods, instance methods operate on the unique attributes of each instance. The class method does not have access to the instance attributes and therefore cannot perform unique functions.

While some traders sell an item at the normal price, some can decide to offer a discount on theirs. The value of the new price is subject to each trader's percentage discount.

Below is an example of an instance method:

class Trader:
    def __init__(self, name):
        self.name = name
        self.selling_price = 500

    @classmethod
    def sell(cls):
        print("A Trader sold an item")

    def offer_discount(self):
        # At 20% discount 
        discount = 0.2 * self.selling_price
        self.selling_price = self.selling_price - discount 
        print(f"{self.name} offers a discount")


trader = Trader("John")
trader.offer_discount()
>>> "John offers a discount"

# New selling price after discount
print(trader.selling_price)
>>> 400

In the example above, there is an extra method named offer_discount(). This method performs a unique function on each instance of the Trader class. It prints out the name of the trader and their new selling price. They also take self as their first parameter allowing them to access and modify the instance's attributes. If you create another instance of the Trader class, their selling price will remain 500. Only John's price differs because he offered a 20% discount.

Static methods

Static methods are defined using the @staticmethod decorator. They don't take an initial argument like class and instance methods. As for the Trader class, a static method can be useful for trade calculations, but does not depend on the attributes or state of a trader instance.

Special methods

These methods have double underscores (__) at the beginning and end of their names, such as __init__(), __str__(), __add__(), etc. They define how instances of a class should behave in specific situations. For example, the __init__() is called when an instance is created, and __str__() defines how the object should be represented as a string (str).

Inheritance in OOP

Definition

Inheritance in OOP refers to a condition where a class inherits from another class. Inheritance is often used when a class needs a method from another class with which it is closely related or shares similarities. The inherited class is called the parent class, while the new class is called child class.

How it works

Let's assume we have a Trader class with attributes such as name, age, daily_income, monthly_income and products, and a sell() method. Inheritance becomes useful when we need to create other subdivisions of the Trader class like Wholesaler or Retailer. To reduce ambiguity and duplication of code, we let these subdivisions inherit from the attributes and methods of the Trader class, rather than creating them from scratch.

Inheritance examples

Below is a code snippet showing how inheritance is implemented:

class Trader:

    def __init__(self, name, daily_income, monthly_income, products):
        self.daily_income = daily_income
        self.name = name
        self.monthly_income = monthly_income
        self.products = products

    def sell(self):
        print(f"{self.name} sold a product")

class Wholesaler(Trader):
    pass

class Retailer(Trader):
    pass

max_products = ["Wipes", "Food ingredients", "Soap", "Baby kits", "Groceries"]
wholesaler_max = Wholesaler("Max", 100, "3000", max_products)

wholesaler_max.sell()
>>> "Max sold a product"


print(wholesaler_max.name)
>>> "Max"


print(f"${wholesaler_max.daily_income}")
>>> "$100"

In the code above, there are 3 classes Trader, Wholesaler, Retailer. The Wholesaler and Retailer are child classes that inherit from the Trader class. Therefore, they both have access to the Trader class' methods and __init__() function.

You do not need to create a new __init__() and sell() function for the Wholesaler and Retailer class.

Below is another example:

class Professional:

    def __init__(self, name, age, discipline, experience, salary):
        self.name = name
        self.age = age
        self.discipline = discipline
        self.experience = experience
        self.salary = salary

    def work(self):
        print(f"{self.name} is working")

class Lawyer(Professional):
    pass

class Doctor(Professional):
    pass

class Engineer(Professional):
    pass

lawyer = Lawyer("Jude", 25, "Law", "2 years", 10000)
lawyer.work()
>>> "Jude is working"


print(lawyer.experience)
>>> "2 years"

In the code above, the Professional class refers to all professional workers. It has several attributes and a work method which is common to professional workers. The Lawyer, Engineer and Doctor class inherit from the Professional class.

With inheritance, you can add more professional workers without setting it up from the scratch. They only need to inherit from the Professional class to get started.

Add extra attributes and methods

This section will help you add extra attributes and methods to a child class.

Add extra attributes to a child class

In some cases, you may need to add extra attributes to a child class. For example, you can add a num_cases_solved attribute to the Lawyer class as seen below:

class Professional:

    def __init__(self, name, age, discipline, experience, salary):
        self.name = name
        self.age = age
        self.discipline = discipline
        self.experience = experience
        self.salary = salary

class Lawyer(Professional):

    def __init__(self, name, age, discipline, experience, salary, num_cases_solved):
        super().__init__(name, age, discipline, experience, salary)
        self.num_cases_solved = num_cases_solved

lawyer = Lawyer("Jude", 25, "Law", "2 years", 10000, 50)

print(f"{lawyer.num_cases_solved} cases solved")
>>> "50 cases solved"

In the example above, the super() method represents the parent class. The super().__init__(*args, **kwargs) method initializes the parent's class and its attributes.

Add extra methods to a child class

The code below shows you how to add extra methods to a child class:

class Professional:

    def __init__(self, name, age, discipline, experience, salary):
        self.name = name
        self.age = age
        self.discipline = discipline
        self.experience = experience
        self.salary = salary

    def work(self):
        print(f"{self.name} is working")

class Lawyer(Professional):

    def __init__(self, name, age, discipline, experience, salary):
        super().__init__(name, age, discipline, experience, salary)
        self.num_cases_solved = 0

    # an extra method
    def solve_a_case(self):
        self.num_cases_solved += 1
        print(f"{self.name} solved a case")


emiloju = Lawyer("Emiloju", 25, "Law", "2 years", 2000)

# call the extra method
emiloju.solve_a_case()
>>> "Emiloju solved a case"


emiloju.num_cases_solved
>>> 1

Encapsulation

Definition

Encapsulation means putting something in a small container or enclosing something in a capsule. In OOP, encapsulation is a concept that involves hiding the data or attributes of a class from being modified or accessed directly in a code. The primary goal of encapsulation is to hide the internal details and state of an object while providing controlled access to that object/instance's data and behaviour.

For example, in the Professional class, professionals should not be able to set salaries themselves. Encapsulation is used to implement this.

Encapsulation examples

Study the code below:

class Professional:

    def __init__(self, name):
        self.name = name
        self.salary = 500

    def double_salary(self):
        return self.salary * 2

professional = Professional("Emiloju")
professional.salary = 5000
print(f"${professional.salary}")
>>> "$5000"

In the code above, a professional cannot pass in the salary directly, but they can still change the salary’s value. The same thing applies to every other attribute.

Encapsulation allows you to keep an attribute private by adding a double underscore (__) as prefix. Below is an example:

class Professional:

    def __init__(self, name):
        self.name = name
        self.__salary = 500

    def double_salary(self):
        new_salary = self.__salary * 2
        return f"New salary is ${new_salary}"

professional = Professional("Rilwan")
print(professional.__salary)
>>> "AttributeError"

In the example above, the salary attribute is prefixed with a double underscore (__). You will get an AttributeError when you try to access this attribute. It is only available within the class. In the same vein, if you try to modify it outside the class, it will not change the value of the salary. See the example below:

professional.__salary = 2000
print(professional.double_salary())
>>> "New salary is $1000"

Polymorphism

Definition

Polymorphism in Python and object-oriented programming (OOP) refers to the ability of different objects to respond to the same method or function in a way that is appropriate for their specific type or class. In Python polymorphism is often achieved through method overriding and inheritance. Here's a simple example below:

Example

class Animal:
    def speak(self):
        pass

class Dog(Animal):
    def speak(self):
        return "Woof"

class Cat(Animal):
    def speak(self):
        return "Meow!"

def animal_sound(animal):
    return animal.speak()

dog = Dog()
cat = Cat()

print(animal_sound(dog))
>>> "Woof"


print(animal_sound(cat))
>>> "Meow!"

In this example, both the Dog and Cat classes inherit from the Animal class and override its speak() method. When the animal_sound() function is called with the two types of animals, polymorphism allows it to produce the appropriate sound based on the specific class of the object passed to it.

Conclusion

Object-Oriented Programming (OOP) in Python allows you to build more organized, modular, and scalable applications. By focusing on core concepts like classes, inheritance, encapsulation, and polymorphism, you can design software that’s easier to debug, maintain, and expand.

Whether you're building small scripts or complex applications, OOP principles provide a robust framework that allows you to tackle larger programming challenges effectively. Experiment with different designs, build projects, and refine your skills to unlock the full potential of this programming concept.

Thanks for reading to the end. Before you leave, please don't forget to like, share and follow. See you on the next one.