Interface Driven Programming In Python
Defining an Abstract Base Class
The definition of a ABC is straightforward besides the fact that is using the not so widely concept of metaclasses. The developer does not need to understand exactly what is happening in the background although in one of my next posts I will talk more about metaclasses and how they can be used.
For now you can simply follow some implementation guidelines and understand what ABCs are, strictly from the user scope of view.
Let’s now go back to our example and see how we can improve its functionality..
Our original base class looked like this:
class Shape(object): def get_area(self): pass
Assuming we are using python 2.7, to convert Shape to an ABC is as easy as follows:
from abc import ABCMeta, abstractmethod class Shape(object): __metaclass__= ABCMeta @abstractmethod def get_area(self): pass
As you can see the necessary steps to follow are the following:
- Import ABCMeta and abstractmethod from abc
- Specify the metaclass of the class as ABCMeta
- Decorate the functions that we need to become abstract with the @abstractmethod decorator.
That’s it! Shape has now become an abstract class!
How an ABC differs from any other class
An ABC cannot be instantiated directly
After defining Shape as an ABC, code like the following:
some_shape = Shape()
will fail to execute throwing the following TypeError:
TypeError: Can't instantiate abstract class Shape with abstract methods get_area
An ABC is meant to serve only as a base class leaving the implementation of its abstract members to its descendants. This does not mean that an ABC cannot contain implementation details, in other words we can define members of the ABC as in any other class, assuming that they are not decorated by the abstractmethod (or absractproperty that we will see later) decorators.
An ABC is forcing its descendants to implement its abstract parts
A descendant of an ABC is forced to implement its abstract parts. Code like the following:
class BogusShape(Shape): pass b = BogusShape()
Will fail with the following exception:
TypeError: Can't instantiate abstract class BogusShape with abstract methods get_area
Note that the TypeError exception will be thrown at the time we will try to instantiate BogusShape. In other languages like C++ or Java the error condition would had been caught at compile time.
Defining descendants of an ABC
A descendant of an ABC needs to implement all of its abstract parts. Failure to do so will cause an exception to be thrown at run time. For example the following two classes are valid implementers of the ABC Shape:
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
As you can see, there is nothing specific to the fact that the base class in an ABC.
Using this approach our original code can be written as follows:
#!/usr/bin/python from abc import ABCMeta, abstractmethod class Shape(object): __metaclass__= ABCMeta @abstractmethod 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 class Ellipse: def __init__(self, a, b): self.a, self.b = a,b def get_area(self): return self.a * self.b * 3.14 def find_total_area(shapes): return sum(shape.get_area() for shape in shapes if isinstance(shape, Shape)) shapes = [Rectangle(10.2, 8.3), Circle(6.2), Ellipse(8,12)] print find_total_area(shapes)
An interesting feature of ABCs is that if we want Ellipse to be considered as a Shape as well, it is possible to do even without deriving from Shape, as long it implements get_area. This can be accomplished by adding the following line of code:
Shape.register(Ellipse)
Now our program becomes:
#!/usr/bin/python from abc import ABCMeta, abstractmethod class Shape(object): __metaclass__= ABCMeta @abstractmethod 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 class Ellipse: def __init__(self, a, b): self.a, self.b = a,b def get_area(self): print 'Ellipse get_area was called!' return self.a * self.b * 3.14 def find_total_area(shapes): return sum(shape.get_area() for shape in shapes if isinstance(shape, Shape)) shapes = [Rectangle(10.2, 8.3), Circle(6.2), Ellipse(8,12)] print find_total_area(shapes) Shape.register(Ellipse) print find_total_area(shapes)
The output of our program now, will be the following:
205.3616 Ellipse get_area was called! 506.8016
As you can see, the Ellipse.get_area was now called even if Ellipse does not derives from Shape, since we called the register function of the ABC.
Conclusion
Abstract base classes can be used to customize type checking whenever something like this is preferable over the classical pythonic duck typing.
We can use this technique in cases where we want to favour code extensibility based in loose coupled components, that will be developed by many developers who do not necessary have a deep understanding of the code details and prefer to view specific components as black boxes that they interact with using well defined interface-based contracts.
Another argument in favour of ABCs based design has to do with the improved documentation which can become higher level, encapsulating as many implementation details as possible from the user of the component allowing for easier code changes and testing.