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.

2 comments:

Chris Leary said...

You should check out Zope interfaces -- lots of really smart Python guys in that shop. http://wiki.zope.org/Interfaces/InterfacesUseCases

Allows for invariants, pre- and post-conditions.

I'm not sure I like your UseInterface solution -- Python is all about gentleman's privacy, and you're creating an adapter class purposefully to make runtime attribute restrictions. I have to mull it over a bit, though.

McJohn said...

I don't really think this violates the privacy of the other object, though. In fact, this is probably less invasive than most solutions in python--we're not requiring that you derive from a specific object, just that you match the interface that object would provide.

For example, duck typing would still work as expected.

The point here is really to ensure that--as someone providing an API framework--we can't cheat and say "I've said that I want a Widget object, but I know this is really going to be a WidgetImplementation, so I'll call this function".

This is something that would just happen in a statically typed language, but could be quite problematic in python.