No history yet

Advanced Object-Oriented Programming

Beyond a Single Parent

In object-oriented programming, inheritance allows a new class to take on the properties and methods of an existing class. We've seen how a Poodle class can inherit from a Dog class. But what if a class needs to combine features from multiple, distinct parents? This is where multiple inheritance comes in.

Multiple inheritance enables a class to inherit from more than one parent class, mixing functionalities from different sources.

Imagine you're designing a video game character, a Griffin. A griffin has the body of a lion and the head and wings of an eagle. It can both walk and fly. Instead of writing code for walking and flying from scratch, we can inherit from existing Walker and Flyer classes.

class Walker:
    def walk(self):
        return "Walking on the ground."

class Flyer:
    def fly(self):
        return "Soaring through the sky."

# The Griffin class inherits from both Walker and Flyer
class Griffin(Walker, Flyer):
    pass

# Create a Griffin object
griffy = Griffin()

print(griffy.walk())  # Output: Walking on the ground.
print(griffy.fly())   # Output: Soaring through the sky.

Just like that, our Griffin class has the abilities of both its parents. This is a powerful way to create flexible and reusable code. However, it can also introduce a tricky problem.

The Diamond Problem

What happens if two parent classes have a method with the same name? Which one does the child class use? This ambiguity can lead to a specific issue called the "diamond problem." It occurs when a class inherits from two classes that both inherit from the same grandparent class.

If both Walker and Flyer have an eat method they inherited and possibly overrode from Organism, which eat method should Griffin use? Python solves this with a clear set of rules called the Method Resolution Order (MRO).

MRO

noun

The Method Resolution Order is the sequence in which Python searches for a method in a class's hierarchy.

The MRO follows a specific algorithm (C3 linearization) that ensures a predictable and consistent order. It searches the current class first, then moves to its parent classes from left to right. You can inspect the MRO for any class using the __mro__ attribute.

class Organism:
    def eat(self):
        return "Eating as an organism."

class Walker(Organism):
    def eat(self):
        return "Eating while walking."

class Flyer(Organism):
    def eat(self):
        return "Eating while flying."

class Griffin(Walker, Flyer):
    pass

griffy = Griffin()
print(griffy.eat())  # Output: Eating while walking.

# Let's inspect the MRO
print(Griffin.__mro__)
# Output: (<class '__main__.Griffin'>, <class '__main__.Walker'>, <class '__main__.Flyer'>, <class '__main__.Organism'>, <class 'object'>)

As you can see, Python checks Griffin, then Walker, then Flyer, and finally Organism. Since it finds an eat method in Walker first, it uses that one. The order in which you list the parent classes (Walker, Flyer) matters!

Creating Blueprints with ABCs

Sometimes you want to define a template for other classes to follow. You might want to say, "Any class that claims to be a Shape must have a way to calculate its area." This is the job of an Abstract Base Class (ABC).

An ABC defines a common interface that all its subclasses must implement. You cannot create an instance of an ABC itself; it only serves as a blueprint.

To create an ABC in Python, you use the abc module. By marking a method with the @abstractmethod decorator, you force all subclasses to provide their own implementation of that method. If a subclass fails to do so, Python will raise a TypeError.

import abc

class Shape(abc.ABC):
    @abc.abstractmethod
    def area(self):
        """Return the area of the shape."""
        pass

class Square(Shape):
    def __init__(self, side):
        self.side = side

    def area(self):
        return self.side * self.side

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    # This class doesn't implement area(), so it's invalid!

# This works
square = Square(5)
print(square.area()) # Output: 25

# This will raise a TypeError
# circle = Circle(3)

In the example, trying to create an instance of Circle would fail because it doesn't implement the area method required by the Shape ABC. This is a powerful way to enforce structure in your code and ensure different objects have a consistent set of features.

The Magic of Dunder Methods

You've already seen methods like __init__. These methods with double underscores at the beginning and end are called "dunder" methods (for double underscore). They are also known as magic methods because they allow your objects to integrate with Python's core syntax and behavior.

Dunder methods let you define what happens when you use standard operators like +, ==, or functions like len() and str() on your objects. This makes your custom objects feel just as natural to use as Python's built-in types.

Dunder MethodOperator/FunctionDescription
__str__(self)str(obj)Returns a user-friendly string representation of the object.
__len__(self)len(obj)Returns the length of the object.
__add__(self, other)obj1 + obj2Defines behavior for the addition operator.
__eq__(self, other)obj1 == obj2Defines behavior for the equality operator.

Let's create a Vector class to represent a 2D vector and see how dunder methods bring it to life.

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __str__(self):
        # Friendly string for printing
        return f"Vector({self.x}, {self.y})"

    def __add__(self, other):
        # Add two vectors together
        new_x = self.x + other.x
        new_y = self.y + other.y
        return Vector(new_x, new_y)

    def __eq__(self, other):
        # Check if two vectors are equal
        return self.x == other.x and self.y == other.y

v1 = Vector(2, 3)
v2 = Vector(4, 5)
v3 = Vector(6, 8)

print(v1)        # Uses __str__ -> Output: Vector(2, 3)

sum_v = v1 + v2  # Uses __add__
print(sum_v)     # Uses __str__ -> Output: Vector(6, 8)

print(sum_v == v3) # Uses __eq__ -> Output: True

Without dunder methods, trying to add two Vector objects with + would cause an error. By implementing __add__, we tell Python exactly how to handle this operation. These advanced OOP tools allow you to write code that is not only powerful and reusable but also clean and intuitive.

Ready to test your knowledge?

Quiz Questions 1/5

What is the primary benefit of using multiple inheritance in object-oriented programming?

Quiz Questions 2/5

Consider the following Python code:

class A:
    def ping(self):
        return "A"

class B(A):
    def ping(self):
        return "B"

class C(A):
    pass

class D(B, C):
    pass

d = D()
print(d.ping())

What will be the output?

By mastering multiple inheritance, MRO, ABCs, and dunder methods, you gain finer control over your code's structure and behavior, paving the way for more sophisticated and professional software design.