There are few frameworks that allow you to build explicit interfaces in Python. The most notable one is a part of the Zope project. It is the zope.interface package. Although nowadays, Zope is not as popular as it used to be, the zope.interface package is still one of the main components of the Twisted framework.
The core class of the zope.interface package is the Interface class. It allows you to explicitly define a new interface by subclassing. Let's assume that we want to define the obligatory interface for every implementation of a rectangle:
from zope.interface import Interface, Attribute class IRectangle(Interface): width = Attribute("The width of rectangle") height = Attribute("The height of rectangle") def area(): """ Return area of rectangle """ def perimeter(): """ Return perimeter of rectangle """
Here are some important things to remember when defining interfaces with zope.interface:
- The common naming convention for interfaces is to use I as the name prefix.
- The methods of an interface must not take the self parameter.
- As the interface does not provide concrete implementation, it should consist only of empty methods. You can use the pass statement, raise NotImplementedError, or provide docstring (preferred).
- An interface can also specify the required attributes using the Attribute class.
When you have such a contract defined, you can then define new concrete classes that provide an implementation for our IRectangle interface. In order to do that, you need to use the implementer() class decorator and implement all of the defined methods and attributes:
@implementer(IRectangle) class Square: """ Concrete implementation of square with rectangle interface """ def __init__(self, size): self.size = size @property def width(self): return self.size @property def height(self): return self.size def area(self): return self.size ** 2 def perimeter(self): return 4 * self.size @implementer(IRectangle) class Rectangle: """ Concrete implementation of rectangle """ def __init__(self, width, height): self.width = width self.height = height def area(self): return self.width * self.height def perimeter(self): return self.width * 2 + self.height * 2
It is common to say that the interface defines a contract that a concrete implementation needs to fulfil. The main benefit of this design pattern is being able to verify consistency between contract and implementation before the object is being used. With the ordinary duck-typing approach, you only find inconsistencies when there is a missing attribute or method at runtime. With zope.interface, you can introspect the actual implementation using two methods from the zope.interface.verify module to find inconsistencies early on:
- verifyClass(interface, class_object): This verifies the class object for the existence of methods and correctness of their signatures without looking for attributes.
- verifyObject(interface, instance): This verifies the methods, their signatures, and also attributes of the actual object instance.
Since we have defined our interface and two concrete implementations, let's verify their contracts in an interactive session:
>>> from zope.interface.verify import verifyClass, verifyObject >>> verifyObject(IRectangle, Square(2)) True >>> verifyClass(IRectangle, Square) True >>> verifyObject(IRectangle, Rectangle(2, 2)) True >>> verifyClass(IRectangle, Rectangle) True
This is nothing impressive. The Rectangle and Square classes carefully follow the defined contract, so there is nothing more to see than a successful verification. But what happens when we make a mistake? Let's see an example of two classes that fail to provide full IRectangle interface implementation:
@implementer(IRectangle) class Point: def __init__(self, x, y): self.x = x self.y = y @implementer(IRectangle) class Circle: def __init__(self, radius): self.radius = radius def area(self): return math.pi * self.radius ** 2 def perimeter(self): return 2 * math.pi * self.radius
The Point class does not provide any method or attribute of the IRectangle interface, so its verification will show inconsistencies already on the class level:
>>> verifyClass(IRectangle, Point) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "zope/interface/verify.py", line 102, in verifyClass return _verify(iface, candidate, tentative, vtype='c') File "zope/interface/verify.py", line 62, in _verify raise BrokenImplementation(iface, name) zope.interface.exceptions.BrokenImplementation: An object has failed to implement interface <InterfaceClass __main__.IRectangle> The perimeter attribute was not provided.
The Circle class is a bit more problematic. It has all the interface methods defined, but breaks the contract on the instance attribute level. This is the reason, in most cases, that you need to use the verifyObject() function to completely verify the interface implementation:
>>> verifyObject(IRectangle, Circle(2)) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "zope/interface/verify.py", line 105, in verifyObject return _verify(iface, candidate, tentative, vtype='o') File "zope/interface/verify.py", line 62, in _verify raise BrokenImplementation(iface, name) zope.interface.exceptions.BrokenImplementation: An object has failed to implement interface <InterfaceClass __main__.IRectangle> The width attribute was not provided.
Using zope.inteface is an interesting way to decouple your application. It allows you to enforce proper object interfaces without the need for the overblown complexity of multiple inheritances, and also allows you to catch inconsistencies early. But the biggest downside of this approach is the requirement to explicitly define that the given class follows some interface in order to be verified. This is especially troublesome if you need to verify instances coming from the external classes of built-in libraries. zope.interface provides some solutions for that problem and you can, of course, handle such issues on your own by using the adapter pattern, or even monkey-patching. Anyway, the simplicity of such solutions is at least debatable.