Interface Driven Programming In Python

A simple example leading to the use of an Abstract Base Class
To better understand the need of an Abstract Base Class, let’s start with a simple example consisting of a base class called Shape which defines a single method called get_area and a couple of its descendants called Rectangle and Circle:


class Shape(object):
    def get_area(self):
        pass

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def get_area(self):
        return self.width * self.height

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

    def get_area(self):
        return self.radius **2 * 3.14

This code looks good from the first glance, but as we inspect it closer we can see some possible pitfalls. For example lets assume that we need a function with the name find_total_area receiving a collection of shapes and returning the total area covered by all of them, something like the following:

def find_total_area(shapes):
    return sum(shape.get_area() for shape in shapes )

A possible use of this function could be the following:

shapes = [Rectangle(10.2, 8.3), Circle(6.2)]
print find_total_area(shapes)

which will correctly output the following:

205.3616

Problems with this implementation

A problem we can immediately see with the implementation of find_total_area has to do with the user passing a list of objects not supporting the get_area functionality. For example there might exist an Ellipse class implemented as follows:

class Ellipse:
    def __init__(self, a, b):
        self.a, self.b = a,b

    def calculate_area(self):
        return self.a * self.b * 3.14

and our user now might do something like the following:

shapes = [Rectangle(10.2, 8.3), Circle(6.2), Ellipse(3,8) ]
print find_total_area(shapes)

As expected, in this case the find_total_area will throw an exception since the Ellipse object does not support the get_area function…

One (not so good though!) way of fixing this problem, would had been to re-implement the find_total_area function checking for each shape to see if it supports the get_area functionality, as can be seen here:

def find_total_area(shapes):
    def find_total_area(shapes):
    return sum(shape.get_area() for shape in shapes 
                                if hasattr(shape, 'get_area')
              )

Now our code will run happily without throwing any exceptions, but we still have some problems. First of all we silently ignore the Ellipse in our calculations and secondly we are making a silent assumption that the get_area returns a numeric value that can be accumulated by sum. This can become a problem for our implementation, as can be seen in the following user of our code:

shapes = [Rectangle(10.2, 8.3), Circle(6.2), Shape() ]
print find_total_area(shapes)

In this case, our modified version of find_total_area, will try to call the get_area as it is implemented in Shape and of course throw an exception since it is returning a None value that cannot be added to the areas of the other shapes.

A better solution to our problem can be implemented by using an Abstract Base Class, which we will see in the next page…

Leave a Reply