DEV Community

Usool
Usool

Posted on

Mastering Python Object-Oriented Programming (OOP): A Comprehensive Guide with Examples

Introduction

Object-Oriented Programming (OOP) is one of the most popular programming paradigms used in modern software development. It allows you to model real-world entities using classes and objects, making code reusable, modular, and scalable. In this blog post, we will explore Python's OOP concepts from basic to advanced, using a single use-case example: building an inventory management system for an online store.

Why Use Object-Oriented Programming?

  • Modularity: Code is broken into self-contained modules.
  • Reusability: Once a class is written, you can use it anywhere in your program or future projects.
  • Scalability: OOP allows programs to grow by adding new classes or extending existing ones.
  • Maintainability: Easier to manage, debug, and extend.

Basic Concepts: Classes and Objects

What Are Classes and Objects?

  • Class: A blueprint for creating objects (instances). It defines a set of attributes (data) and methods (functions) that the objects will have.
  • Object: An instance of a class. You can think of objects as real-world entities that have state and behavior.

Example: Inventory Item Class

Let’s start by creating a class to represent an item in our online store's inventory. Each item has a name, price, and quantity.

class InventoryItem:
    def __init__(self, name, price, quantity):
        self.name = name
        self.price = price
        self.quantity = quantity

    def get_total_price(self):
        return self.price * self.quantity

# Creating objects of the class
item1 = InventoryItem("Laptop", 1000, 5)
item2 = InventoryItem("Smartphone", 500, 10)

# Accessing attributes and methods
print(f"Item: {item1.name}, Total Price: ${item1.get_total_price()}")
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • The __init__ method is the constructor, used to initialize the object’s attributes.
  • self refers to the current instance of the class, allowing each object to maintain its own state.
  • The method get_total_price() calculates the total price based on the item’s price and quantity.

Encapsulation: Controlling Access to Data

What is Encapsulation?

Encapsulation restricts direct access to an object's attributes to prevent accidental modification. Instead, you interact with the object's data through methods.

Example: Using Getters and Setters

We can improve the InventoryItem class by making the attributes private and controlling access using getter and setter methods.

class InventoryItem:
    def __init__(self, name, price, quantity):
        self.__name = name  # Private attribute
        self.__price = price
        self.__quantity = quantity

    def get_total_price(self):
        return self.__price * self.__quantity

    # Getter methods
    def get_name(self):
        return self.__name

    def get_price(self):
        return self.__price

    def get_quantity(self):
        return self.__quantity

    # Setter methods
    def set_price(self, new_price):
        if new_price > 0:
            self.__price = new_price

    def set_quantity(self, new_quantity):
        if new_quantity >= 0:
            self.__quantity = new_quantity

# Example usage
item = InventoryItem("Tablet", 300, 20)
item.set_price(350)  # Update price using the setter method
print(f"Updated Price of {item.get_name()}: ${item.get_price()}")
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • Attributes are now private (__name, __price, __quantity).
  • You access or modify these attributes using getter and setter methods, protecting the internal state of the object.

Inheritance: Reusing Code in New Classes

What is Inheritance?

Inheritance allows one class (child) to inherit attributes and methods from another class (parent). This promotes code reusability and hierarchy modeling.

Example: Extending the Inventory System

Let’s extend our system to handle different types of inventory items, such as PerishableItem and NonPerishableItem.

class InventoryItem:
    def __init__(self, name, price, quantity):
        self.__name = name
        self.__price = price
        self.__quantity = quantity

    def get_total_price(self):
        return self.__price * self.__quantity

    def get_name(self):
        return self.__name

class PerishableItem(InventoryItem):
    def __init__(self, name, price, quantity, expiration_date):
        super().__init__(name, price, quantity)
        self.__expiration_date = expiration_date

    def get_expiration_date(self):
        return self.__expiration_date

class NonPerishableItem(InventoryItem):
    def __init__(self, name, price, quantity):
        super().__init__(name, price, quantity)

# Example usage
milk = PerishableItem("Milk", 2, 30, "2024-09-30")
laptop = NonPerishableItem("Laptop", 1000, 10)

print(f"{milk.get_name()} expires on {milk.get_expiration_date()}")
print(f"{laptop.get_name()} costs ${laptop.get_total_price()}")
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • PerishableItem and NonPerishableItem inherit from InventoryItem.
  • The super() function is used to call the parent class's constructor (__init__) to initialize common attributes.
  • PerishableItem adds the new attribute expiration_date.

Polymorphism: Interchangeable Objects

What is Polymorphism?

Polymorphism allows objects of different classes to be treated as instances of a common superclass. It enables you to define methods in the parent class that are overridden in the child classes, but you can call them without knowing the exact class.

Example: Polymorphic Behavior

Let’s modify our system so that we can display different information based on the item type.

class InventoryItem:
    def __init__(self, name, price, quantity):
        self.__name = name
        self.__price = price
        self.__quantity = quantity

    def display_info(self):
        return f"Item: {self.__name}, Total Price: ${self.get_total_price()}"

    def get_total_price(self):
        return self.__price * self.__quantity

class PerishableItem(InventoryItem):
    def __init__(self, name, price, quantity, expiration_date):
        super().__init__(name, price, quantity)
        self.__expiration_date = expiration_date

    def display_info(self):
        return f"Perishable Item: {self.get_name()}, Expires on: {self.__expiration_date}"

class NonPerishableItem(InventoryItem):
    def display_info(self):
        return f"Non-Perishable Item: {self.get_name()}, Price: ${self.get_total_price()}"

# Example usage
items = [
    PerishableItem("Milk", 2, 30, "2024-09-30"),
    NonPerishableItem("Laptop", 1000, 10)
]

for item in items:
    print(item.display_info())  # Polymorphic method call
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • The display_info() method is overridden in both PerishableItem and NonPerishableItem.
  • When you call display_info() on an object, Python uses the appropriate method depending on the object’s type, even if the specific type is unknown at runtime.

Advanced Concepts: Decorators and Class Methods

Class Methods and Static Methods

  • Class methods are methods that belong to the class itself, not to any object. They are marked with @classmethod.
  • Static methods do not access or modify class or instance-specific data. They are marked with @staticmethod.

Example: Managing Inventory with Class Methods

Let’s add a class-level method to track the total number of items in the inventory.

class InventoryItem:
    total_items = 0  # Class attribute to keep track of all items

    def __init__(self, name, price, quantity):
        self.__name = name
        self.__price = price
        self.__quantity = quantity
        InventoryItem.total_items += quantity  # Update total items

    @classmethod
    def get_total_items(cls):
        return cls.total_items

# Example usage
item1 = InventoryItem("Laptop", 1000, 5)
item2 = InventoryItem("Smartphone", 500, 10)

print(f"Total items in inventory: {InventoryItem.get_total_items()}")
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • total_items is a class attribute shared by all instances.
  • The class method get_total_items() allows us to access this shared data.

Decorators in OOP

Decorators in OOP are often used to modify the behavior of methods. For example, we can create a decorator to automatically log method calls in our inventory system.

def log_method_call(func):
    def wrapper(*args, **kwargs):
        print(f"Calling method {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

class InventoryItem:
    def __init__(self, name, price, quantity):
        self.__name = name
        self.__price = price
        self.__quantity = quantity

    @log_method_call
    def get_total_price(self):
        return self.__price * self.__quantity

# Example usage
item = InventoryItem("Tablet", 300, 10)
print(item.get

_total_price())  # Logs the method call
Enter fullscreen mode Exit fullscreen mode

Conclusion

Object-Oriented Programming in Python offers a powerful and flexible way to organize and structure your code. By using classes, inheritance, encapsulation, and polymorphism, you can model complex systems and enhance code reusability. In this post, we walked through key OOP concepts, gradually building an inventory management system to demonstrate each principle.

OOP in Python makes your code more readable, maintainable, and easier to extend as the complexity of your application grows. Happy coding!

Top comments (0)