python OOP inheritance abstract-classes polymorphism

Advanced OOP: Inheritance, Abstract Classes and Polymorphism

Learning objectives

In this second in-depth part you will learn to:

  • Define class hierarchies with Inheritance.
  • Use Abstract Classes to enforce rules on subclasses.
  • Use Static Methods for utilities decoupled from the instance.
  • Implement Operator Overloading to compare objects (e.g. <, ==).

Abstract Classes and Inheritance

We often want to define a “base model” that cannot be used directly, but only serves as a foundation for more specific classes.

The abc module (Abstract Base Classes)

Imagine we want to manage both IPv4 and IPv6 addresses. We create a parent class IPAddress that forces child classes to implement certain methods.

Class hierarchy:

from abc import ABC, abstractmethod

# Abstract Class (inherits from ABC)
class IPAddress(ABC):
    def __init__(self, address):
        self.address = address

    # Abstract method: children MUST implement it
    @abstractmethod
    def to_binary(self):
        pass

    # Concrete method: children inherit it ready to use
    def __str__(self):
        return f"IP: {self.address}"

# Concrete Subclass
class IPv4(IPAddress):
    def to_binary(self):
        # IPv4-specific logic
        octets = self.address.split('.')
        binary = [format(int(x), '08b') for x in octets]
        return "".join(binary)

If you tried to instantiate IPAddress directly or create a subclass without to_binary, Python would raise an error.

Static Methods (@staticmethod)

Sometimes you need a function that logically belongs to the class but does not need to access self (i.e. the instance’s data). They are often used for validation or utility functions.

class IPv4(IPAddress):
    # ... previous methods ...

    @staticmethod
    def is_valid_format(ip_str):
        """Checks whether a string looks like a valid IPv4 address."""
        parts = ip_str.split('.')
        if len(parts) != 4:
            return False
        for p in parts:
            if not p.isdigit() or not (0 <= int(p) <= 255):
                return False
        return True

# We can call it WITHOUT creating an object!
print(IPv4.is_valid_format("192.168.1.1"))  # True
print(IPv4.is_valid_format("300.0.0.1"))    # False

Operator Overloading and Comparators

In Python there is no method overloading (same name, different parameters) as in Java, but Operator Overloading is very powerful. We can redefine how objects respond to standard operators (+, -, ==, <) using Magic Methods.

Comparison (__eq__, __lt__)

We want to be able to sort a list of IPs or ask if ip1 > ip2.

class IPv4(IPAddress):
    # ...

    def __eq__(self, other):
        """Defines the behaviour of the == operator"""
        if not isinstance(other, IPv4):
            return False
        return self.address == other.address

    def __lt__(self, other):
        """Defines the behaviour of the < operator (Less Than)"""
        # Trick: compare integer values or binary strings
        return self.to_binary() < other.to_binary()

# Usage
ip_a = IPv4("10.0.0.1")
ip_b = IPv4("192.168.1.1")

if ip_a < ip_b:
    print(f"{ip_a} comes before {ip_b}")

# Automatic sorting in lists!
ip_list = [ip_b, ip_a]
ip_list.sort()  # Uses __lt__ automatically
print([str(ip) for ip in ip_list])

Overloading Mathematical Operators

We can also define addition. For example, adding an integer to an IP to get the next IP.

    def __add__(self, n):
        """Handles: new_ip = ip_object + integer"""
        # 1. Convert IP to integer
        # 2. Add n
        # 3. Convert back to string and return new object
        # (Implementation left as exercise)
        pass

Conceptual Summary

  • Inheritance: class B(A). “B is a type of A”. Avoids rewriting common code.
  • Abstract Class: An “incomplete” class that defines the rules for its children.
  • Static Method: A function inside a class that does not use self.
  • Overloading: Giving new meanings to operators +, ==, < for our objects.

Advanced Exercise

Extend the IPv4 class created above:

  1. Implement __add__: make IPv4("192.168.1.1") + 2 return a new IPv4("192.168.1.3") object.
  2. Implement __sub__: the difference between two IPs should return the number of hosts between them (an integer).
  3. Create a Network class that takes an IP and a mask, and has a method to generate all usable IPs (using your + operator).