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