Interfaces in Python
There’s been a long history of proposals and disagreement over interfaces in Python. I’m going to ignore all of that and show one way to utilize interfaces.
An interface can be a class from which implementing classes inherit:
# interfaces.py:
class CountFishInterface(object):
def one(self, *args, **kwargs):
raise NotImplementedError
def two(self, *args, **kwargs):
raise NotImplementedError
class ColorFishInterface(object):
def red(self, *args, **kwargs):
raise NotImplementedError
def blue(self, *args, **kwargs):
raise NotImplementedError
# models.py:
from interfaces import CountFishInterface, ColorFishInterface
class OurFish(CountFishInterface, ColorFishInterface):
passNow, OurFish doesn’t yet implement the interface. Before we do that, let’s add some tests. Note that since multiple classes may implement our interfaces, we make abstract tests for each interface.
# tests.py:
from unittest import TestCase
from interfaces import CountFishInterface, ColorFishInterface
from models import OurFish
class AbstractTestCountFishInterface(object):
def test_one(self):
try:
self.obj.one()
except NotImplementedError:
self.fail(
str(type(self.obj)) + 'does not implement one'
)
def test_two(self):
try:
self.obj.two()
except NotImplementedError:
self.fail(
str(type(self.obj)) + 'does not implement two'
)
class AbstractTestColorFishInterface(object):
def test_red(self):
try:
self.obj.red()
except NotImplementedError:
self.fail(
str(type(self.obj)) + 'does not implement red'
)
def test_blue(self):
try:
self.obj.blue()
except NotImplementedError:
self.fail(
str(type(self.obj)) + 'does not implement blue'
)
class TestOurFish(AbstractTestCountFishInterface,
AbstractTestColorFishInterface,
TestCase):
def setUp(self):
self.obj = OurFish()Now our tests should fail! Let’s implement the interface in OurFish:
# models.py:
from interfaces import CountFishInterface, ColorFishInterface
class OurFish(CountFishInterface, ColorFishInterface):
def one(self):
return 1
def two(self):
return 2
def red(self):
return '#FF0000'
def blue(self):
return '#0000FF'Now our tests should pass!
Declaring new interfaces and writing the tests is quite verbose. Here’s a simpler way of declaring new interfaces:
# interfaces.py
def Interface(interface_name, method_names):
def interface_helper(*args, **kwargs):
raise NotImplementedError
methods = {method_name: interface_helper for method_name in method_names}
return type(interface_name, (object,), methods)
ColorFishInterface = Interface('ColorFishInterface', [
'red',
'blue'
])That’s not that pythonic looking, but here’s another way to do it:1
# interfaces.py
interfaces = {
'CountFishInterface': ['one', 'two'],
'ColorFishInterface': ['red', 'blue']
}
for interface_name, methods in interfaces.iteritems():
globals()[interface_name] = Interface(interface_name, methods)Still messy, but it makes it easy to add more interfaces.
I’ll leave refactoring the test cases as an exercise to the reader. Beyond moving the try-block into a helper method, the best solution I can presently come up with is code-generation.
Update: No one noticed, but I was originally missing the call to
iteritems!↩
kms