Package: implements
Pythonic interfaces using decoratorsDecorate your implementation class with@implements(<InterfaceClass>).
That’s it!.implementswill ensure that your implementation satisfies
attributes, methods and their signatures as defined in your interface.Moreover, interfaces are enforced via composition. Implementations
don’t inherit interfaces. Your MROs remain untouched and interfaces
are evaluated early during import instead of class instantiation.InstallImplements is available on PyPI and can be installed withpip:pip install implementsNote Python 3.6+ is required as it relies on new features ofinspectmodule.AdvantagesFavor composition over inheritance.Inheriting from multiple classes can be problematic, especially when the superclasses have the same method name but different signatures. Implements will throw a descriptive error if that happens to ensure integrity of contracts.The decorators are evaluated at import time. Any errors will be raised then and not when an object is instantiated or a method is called.It’s cleaner. Using decorators makes it clear we want shared behavior. Also, arguments are not allowed to be renamed.UsageWith _implements_, implementation classes and interface classes must have their own independent class hierarchies. Unlike common patterns, the implementation class must not inherit from an interface class. From version0.3.0and onwards, this condition is checked automatically and an error is raised on a violation.fromimplementsimportInterface,implementsclassDuck:def__init__(self,age):self.age=ageclassFlyable(Interface):@staticmethoddefmigrate(direction):passdeffly(self)->str:passclassQuackable(Interface):deffly(self)->bool:passdefquack(self):pass@implements(Flyable)@implements(Quackable)classMallardDuck(Duck):def__init__(self,age):super(MallardDuck,self).__init__(age)defmigrate(self,dir):returnTruedeffly(self):passThe above would throw the following errors:NotImplementedError:'MallardDuck'mustimplementmethod'fly((self) -> bool)'definedininterface'Quackable'NotImplementedError:'MallardDuck'mustimplementmethod'quack((self))'definedininterface'Quackable'NotImplementedError:'MallardDuck'mustimplementmethod'migrate((direction))'definedininterface'Flyable'You can find a more detailed example inexample.pyand by looking attests.py.JustificationThere are currently two idiomatic ways to rewrite the above example.The first way is to write base classes with mixins raisingNotImplementedErrorin each method.classDuck:def__init__(self,age):self.age=ageclassFlyable:@staticmethoddefmigrate(direction):raiseNotImplementedError("Flyable is an abstract class")deffly(self)->str:raiseNotImplementedError("Flyable is an abstract class")classQuackable:deffly(self)->bool:raiseNotImplementedError("Quackable is an abstract class")defquack(self):raiseNotImplementedError("Quackable is an abstract class")classMallardDuck(Duck,Quackable,Flyable):def__init__(self,age):super(MallardDuck,self).__init__(age)defmigrate(self,dir):returnTruedeffly(self):passBut there are a couple drawbacks implementing it this way:We would only get aNotImplementedErrorwhen callingquackwhich can happen much later during runtime. Also, raisingNotImplementedErroreverywhere looks clunky.It’s unclear without checking each parent class where super is being called.Similarly the return types offlyinFlyableandQuackableare different. Someone unfamiliar with Python would have to read up onMethod Resolution Order.The writer ofMallardDuckmade methodmigratean instance method and renamed the argument todirwhich is confusing.We really want to be differentiating between behavior and inheritance.The advantage of using implements is it looks cleaner and you would get errors at import time instead of when the method is actually called.Another way is to use abstract base classes from the built-inabcmodule:fromabcimportABCMeta,abstractmethod,abstractstaticmethodclassDuck(metaclass=ABCMeta):def__init__(self,age):self.age=ageclassFlyable(metaclass=ABCMeta):@abstractstaticmethoddefmigrate(direction):pass@abstractmethoddeffly(self)->str:passclassQuackable(metaclass=ABCMeta):@abstractmethoddeffly(self)->bool:pass@abstractmethoddefquack(self):passclassMallardDuck(Duck,Quackable,Flyable):def__init__(self,age):super(MallardDuck,self).__init__(age)defmigrate(self,dir):returnTruedeffly(self):passUsing abstract base classes has the advantage of throwing an error earlier
on instantiation if a method is not implemented; also, there are static analysis
tools that warn if two methods have different signatures. But it doesn’t solve
issues 2-4 and implements will throw an error even earlier in import.
It also in my opinion doesn’t look pythonic.CreditImplementation was inspired by aPRof @elifiner.TestRunning unit tests:make testRunning linter:make lintRunning tox:make test-allLicenseApache License v2