Design patterns are meant to make problem-solving easier, and not to provide you with more layers of complexity. The zope.interface is a great concept and may greatly fit some projects, but it is not a silver bullet. By using it, you may shortly find yourself spending more time on fixing issues with incompatible interfaces for third-party classes and providing never-ending layers of adapters instead of writing the actual implementation. If you feel that way, then this is a sign that something went wrong. Fortunately, Python supports for building a lightweight alternative to the interfaces. It's not a full-fledged solution such as zope.interface or its alternatives, but generally provides more flexible applications. You may need to write a bit more code, but in the end, you will have something that is more extensible, better handles external types, and maybe more future-proof.
Note that Python, at its core, does not have an explicit notion of interfaces, and probably never will have, but it has some of the features that allow building something that resembles the functionality of interfaces. The features are as follows:
- Abstract base classes (ABCs)
- Function annotations
- Type annotations
The core of our solution is abstract base classes, so we will feature them first.
As you probably know, direct type comparison is considered harmful and not pythonic. You should always avoid comparisons, consider the following:
assert type(instance) == list
Comparing types in functions or methods this way completely breaks the ability to pass class subtype as an argument to the function. The slightly better approach is to use the isinstance() function, which will take the inheritance into account:
assert isinstance(instance, list)
The additional advantage of isinstance() is that you can use a larger range of types to check the type compatibility. For instance, if your function expects to receive some sort of sequence as the argument, you can compare it against the list of basic types:
assert isinstance(instance, (list, tuple, range))
And such way of type compatibility checking is OK in some situations but is still not perfect. It will work with any subclass of list, tuple, or range, but will fail if the user passes something that behaves exactly the same as one of these sequence types, but does not inherit from any of them. For instance, let's relax our requirements and say that you want to accept any kind of iterable as an argument. What would you do? The list of basic types that are iterable is actually pretty long. You need to cover list, tuple, range, str, bytes, dict, set, generators, and a lot more. The list of applicable built-in types is long, and even if you cover all of them it will still not allow checking against the custom class that defines the __iter__() method, but inherits directly from object.
And this is the kind of situation where abstract base classes are the proper solution, ABC is a class that does not need to provide a concrete implementation, but instead defines a blueprint of a class that may be used to check against type compatibility. This concept is very similar to the concept of abstract classes and virtual methods known in the C++ language.
Abstract base classes are used for two purposes:
- Checking for implementation completeness
- Checking for implicit interface compatibility
So, let's assume we want to define an interface that ensures that a class has a push() method. We need to create a new abstract base class using a special ABCMeta metaclass and an abstractmethod() decorator from the standard abc module:
from abc import ABCMeta, abstractmethod class Pushable(metaclass=ABCMeta): @abstractmethod def push(self, x): """ Push argument no matter what it means """
The abc module also provides an ABC base class that can be used instead of the metaclass syntax:
from abc import ABCMeta, abstractmethod class Pushable(metaclass=ABCMeta): @abstractmethod def push(self, x): """ Push argument no matter what it means """
Once it is done, we can use that Pushable class as a base class for concrete implementation and it will guard us against instantiation of objects that would have an incomplete implementation. Let's define DummyPushable, which implements all interface methods and IncompletePushable that breaks the expected contract:
class DummyPushable(Pushable): def push(self, x): return class IncompletePushable(Pushable): pass
If you want to obtain the DummyPushable instance, there is no problem because it implements the only required push() method:
>>> DummyPushable() <__main__.DummyPushable object at 0x10142bef0>
But if you try to instantiate IncompletePushable, you will get TypeError because of missing implementation of the interface() method:
>>> IncompletePushable() Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: Can't instantiate abstract class IncompletePushable with abstract methods push
The preceding approach is a great way to ensure implementation completeness of base classes but is as explicit as the zope.interface alternative. The DummyPushable instances are of course also instances of Pushable because Dummy is a subclass of Pushable. But how about other classes with the same methods but not descendants of Pushable? Let's create one and see:
>>> class SomethingWithPush: ... def push(self, x): ... pass ... >>> isinstance(SomethingWithPush(), Pushable) False
Something is still missing. The SomethingWithPush class definitely has a compatible interface but is not considered as an instance of Pushable yet. So, what is missing? The answer is the __subclasshook__(subclass) method that allows you to inject your own logic into the procedure that determines whether the object is an instance of a given class. Unfortunately, you need to provide it by yourself, as abc creators did not want to constrain the developers in overriding the whole isinstance() mechanism. We have full power over it, but we are forced to write some boilerplate code.
Although you can do whatever you want to, usually the only reasonable thing to do in the __subclasshook__() method is to follow the common pattern. The standard procedure is to check whether the set of defined methods are available somewhere in the MRO of the given class:
from abc import ABCMeta, abstractmethod class Pushable(metaclass=ABCMeta): @abstractmethod def push(self, x): """ Push argument no matter what it means """ @classmethod def __subclasshook__(cls, C): if cls is Pushable: if any("push" in B.__dict__ for B in C.__mro__): return True return NotImplemented
With the __subclasshook__() method defined that way, you can now confirm that the instances that implement the interface implicitly are also considered instances of the interface:
>>> class SomethingWithPush: ... def push(self, x): ... pass ... >>> isinstance(SomethingWithPush(), Pushable) True
Unfortunately, this approach to verification of type compatibility and implementation completeness does not take into account the signatures of class methods. So, if the number of expected arguments is different in implementation, it will still be considered compatible. In most cases, this is not an issue, but if you need such fine-grained control over interfaces, the zope.interface package allows for that. As already said, the __subclasshook__() method does not constrain you in adding much more complexity to the isinstance() function's logic to achieve a similar level of control.
The two other features that complement abstract base classes are functioning annotations and type hints. Function annotation is the syntax element described briefly in Chapter 2, Modern Python Development Environments. It allows you to annotate functions and their arguments with arbitrary expressions. As explained in Chapter 2, Modern Python Development Environments, this is only a feature stub that does not provide any syntactic meaning. There is utility in the standard library that uses this feature to enforce any behavior. Anyway, you can use it as a convenient and lightweight way to inform the developer of the expected argument interface. For instance, consider this IRectangle interface rewritten from zope.interface to abstract the base class:
from abc import ( ABCMeta, abstractmethod, abstractproperty ) class IRectangle(metaclass=ABCMeta): @abstractproperty def width(self): return @abstractproperty def height(self): return @abstractmethod def area(self): """ Return rectangle area """ @abstractmethod def perimeter(self): """ Return rectangle perimeter """ @classmethod def __subclasshook__(cls, C): if cls is IRectangle: if all([ any("area" in B.__dict__ for B in C.__mro__), any("perimeter" in B.__dict__ for B in C.__mro__), any("width" in B.__dict__ for B in C.__mro__), any("height" in B.__dict__ for B in C.__mro__), ]): return True return NotImplemented
If you have a function that works only on rectangles, let's say draw_rectangle(), you could annotate the interface of the expected argument as follows:
def draw_rectangle(rectangle: IRectange): ...
This adds nothing more than information to the developer about expected information. And even this is done through an informal contract because, as we know, bare annotations contain no syntactic meaning. But they are accessible at runtime, so we can do something more. Here is an example implementation of a generic decorator that is able to verify interface from function annotation if it is provided using abstract base classes:
def ensure_interface(function): signature = inspect.signature(function) parameters = signature.parameters @wraps(function) def wrapped(*args, **kwargs): bound = signature.bind(*args, **kwargs) for name, value in bound.arguments.items(): annotation = parameters[name].annotation if not isinstance(annotation, ABCMeta): continue if not isinstance(value, annotation): raise TypeError( "{} does not implement {} interface" "".format(value, annotation) ) function(*args, **kwargs) return wrapped
Once it is done, we can create some concrete class that implicitly implements the IRectangle interface (without inheriting from IRectangle) and updates the implementation of the draw_rectangle() function to see how the whole solution works:
class ImplicitRectangle: def __init__(self, width, height): self._width = width self._height = height @property def width(self): return self._width @property def height(self): return self._height def area(self): return self.width * self.height def perimeter(self): return self.width * 2 + self.height * 2 @ensure_interface def draw_rectangle(rectangle: IRectangle): print( "{} x {} rectangle drawing" "".format(rectangle.width, rectangle.height) )
If we feed the draw_rectangle() function with an incompatible object, it will now raise TypeError with a meaningful explanation:
>>> draw_rectangle('foo') Traceback (most recent call last): File "<input>", line 1, in <module> File "<input>", line 101, in wrapped TypeError: foo does not implement <class 'IRectangle'> interface
But if we use ImplicitRectangle or anything else that resembles the IRectangle interface, the function executes as it should:
>>> draw_rectangle(ImplicitRectangle(2, 10)) 2 x 10 rectangle drawing
This is our example implementation of ensure_interface() based on the typechecked() decorator from the typeannotations project that tries to provide runtime-checking capabilities (refer to https://github.com/ceronman/typeannotations). Its source code might give you some interesting ideas about how to process type annotations to ensure runtime interface checking.
The last feature that can be used to complement this interface pattern landscape is type hints. Type hints are described in detail by PEP 484 and were added to the language quite recently. They are exposed in the new typing module and are available from Python 3.5. Type hints are built on top of function annotations and reuse this slightly forgotten syntax feature of Python 3. They are intended to guide type hinting and checking for various yet-to-come Python type checkers. The typing module and PEP 484 document aim to provide a standard hierarchy of types and classes that should be used for describing type annotations.
Still, type hints do not seem to be something revolutionary because this feature does not come with any type checker built in into the standard library. If you want to use type checking or enforce strict interface compatibility in your code, you'll have to integrate some third-party libraries. This is why we won't dig into the details of PEP 484. Anyway, type hints and the documents describing them are worth mentioning because if some extraordinary solution will emerge in the field of type checking in Python, it is highly probable to be based on PEP 484.