Sunday, July 15, 2007

Python: Enforcing Interface Requirements

I write a moderate amount of Python code for work. Not necessarily as much as I'd like, but that's a topic for another post.

A short while back, one of my coworkers asked this question at lunch: "If I'm writing an interface that I want other people to be able to use with their own new types, how do I ensure that I write my code in such a way that they can clearly and easily understand the interface that they need to implement? As an additional problem, how do I ensure that I don't accidentally break my interface requirements?"

Now, if you've written much python code at all, you're probably familiar with the term 'duck typing', which basically exerts that "if it quacks like a duck and walks like a duck, it might as well be a duck." This is probably one of python's greatest strengths as it reduces the syntactic complexity of 'templated' code; by default all functions work with all types that match the implicit interface that is their implementation.

The unfortunate side effect of duck-typing is that in order to see the exact interface that you would need to implement for an API to work with, you'd have to pore through the entire codebase of the API (!!). Clearly, that wouldn't work at all. After a bit of discussion, we came up with the following solution:
  • API Interfaces should begin by asserting that the arguments passed in match a specified, required Interface. This takes care of the first half of the problem, because a developer needs only to look just inside the function to determine which Interface class needs to be implemented (or derived from, if the developer so chooses).
  • The objects passed in should be limited (for the duration of the function) to only expose the explicit members that the interface allows for. This solves the second half of the problem, because it makes it impossible to peek under the covers other than what the Interface allows for.
After some discussion (and mail on the topic), we came up with the following implementation:
class __frozen_iface (object):
def __init__ (self, ifspec, instance):
for member in inspect.getmembers(ifspec):
if member[0].startswith ("__"):
continue

if not inspect.ismethod (member[1]):
continue

instance_attr = getattr (instance, member[0])
if instance_attr.im_func == member[1].im_func:
raise AttributeError, "Class '%s' has interface class implementation of attribute '%s'" % (instance.__class__, member[0])

# bypass our internal __setattr__ since that will raise an exception
object.__setattr__ (self, attr_name, instance_attr)

def __setattr__ (self, name, value):
# prevent anyone from accidentally assigning new attributes
raise AttributeError, "Attempt to set an attribute '%s' for frozen interface class '%s'" % (name, self)

def UseInterface(ifspec, instance):
return __frozen_iface (ifspec, instance)
Then, client code would do something like this:
class Renderable(object):
def Draw(self, context):
abstract # Raises an execption if we get here.

def RenderObject(someRenderable):
someRenderable = UseInterface(Renderable, someRenderable)
dir(someRenderable) # Outputs only 'Draw'
This could also be trivially wrapped into a decorator so your code could look like this:

class Renderable(object):
    @Interface(Context)
def Draw(self, context):
abstract # Raises an execption if we get here.

PS: Thanks to JimR for the cool implementation of __frozen_iface.