Welcome, guest | Sign In | My Account | Store | Cart

In about 70 LOC's we give a simple implementation of (a concept of) interfaces for Python.

Needs Python 2.2

Python, 150 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
"""
The pyInterfaces module.

A simplified interface framework for Python.
"""


__version__ = '1.0'
__needs__ = '2.2'
__author__ = "G. Rodrigues"


class Interface(type):
    """The Interface metaclass."""

    def __init__(cls, name, bases, dct):
        super(Interface, cls).__init__(name, bases, dct)
        attribs = dct.keys()
        #Remove __metaclass__.
        attribs.remove('__metaclass__')
        #Store declared attributes => call super setattr.
        super(Interface, cls).__setattr__('__attributes__', attribs)

    #interfaces are "static" objects => we disallow dynamic changes.
    def __setattr__(cls, name, value):
        raise AttributeError("Cannot bind attributes in interface classes.")

    def __delattr__(cls, name):
        raise AttributeError("Cannot delete attributes in interface classes.")

    def attributes(cls):
        """Returns the list of noncallable attributes's names."""
        #Get mro list of interfaces.
        interfaces = [interface for interface in cls.mro() \
                      if isinstance(interface, Interface)]
        #Build list of attribs.
        attribs = {}
        for interface in interfaces:
            for attrib in interface.__attributes__:
                if not callable(getattr(interface, attrib)):
                    attribs[attrib] = None
        return attribs.keys()

    def callables(cls):
        """Returns the list of callable attributes's names."""
        #Get mro list of interfaces.
        interfaces = [interface for interface in cls.mro() \
                      if isinstance(interface, Interface)]
        #Build list of attribs.
        attribs = {}
        for interface in interfaces:
            for attrib in interface.__attributes__:
                if callable(getattr(interface, attrib)):
                    attribs[attrib] = None
        return attribs.keys()

    def implements(cls, obj):
        """Returns 1 if obj implements interface cls, 0 otherwise."""
        #Check attributes.
        for attrib in cls.attributes():
            try:
                objattrib = getattr(obj, attrib)
            except AttributeError:
                return 0
            else:
                if callable(objattrib):
                    return 0
        #Check callables.
        for attrib in cls.callables():
            try:
                objattrib = getattr(obj, attrib)
            except AttributeError:
                return 0
            else:
                if not callable(objattrib):
                    return 0
        return 1


#Global function.
def implements(obj, *interfaces):
    """Returns 1 if obj implements *all* interfaces, 0 otherwise."""
    for interface in interfaces:
        if not interface.implements(obj):
            return 0
    return 1


#Test code and use cases.
if __name__ == '__main__':
    #Declaring an interface.
    class IStack(object):
        """The IStack interface."""

        __metaclass__ = Interface

        def __init__(self):
            """The initializer."""
            raise NotImplementedError

        def push(self, elem):
            """Push an element into the stack."""
            raise NotImplementedError

        def pop(self):
            """Pop an element from the stack."""
            raise NotImplementedError

    print IStack.__attributes__
    print IStack.attributes()
    print IStack.callables()

    #Are changes disallowed?
    try:
        IStack.__attributes__ = 1
    except AttributeError, x:
        print x

    #Declaring an IStack class.
    class Stack(object):
        """The Stack class implemented via nested tuples."""

        def __init__(self):
            """The initializer."""
            super(Stack, self).__init__()
            self.__head = None

        def push(self, elem):
            self.__head = (elem, self.__head)

        def pop(self):
            if self.__head is not None:
                ret, self.__head = self.__head
                return ret
            else:
                raise IndexError("Cannot get an element from an empty stack.")        

    #Instantiate Stack.
    s = Stack()

    if implements(s, IStack):
        print "Object %r behaves like an %r." % (s, IStack)
    else:
        print "Something is not right."

    #This shows why not checking signatures gives problems...
    if implements(Stack, IStack):
        print "Object %r behaves like an %r and it sucks :-(" % (Stack, IStack)
    else:
        print "Although right, something is not right."

The usual pythonic mode of life, as extensively exemplified in this cookbook, is to apply EAFP (“Easier to Ask Forgiveness than Permission”). If you want to safely use some object’s attributes just ask it if it supports them, handling any ensuing exceptions, e.g.

try: obj.append except AttributeError: raise TypeError("Begone, Cretin! I don't know you!")

As befits a dynamic language, the testing is done at run time – and most of the times that is really all that we need. But the pattern becomes cumbersome if we want to test several things at the same time – and this is what I believe to be the real role for would-be-interfaces in Python: they allow us to ensure that it is “safe” to use an object. One of the cornerstones of OO programming is to code for interface not for type. It follows as night from day that if we want to make sure an object is safe to use we check its interface not its type.

And here our Simplified Interface Python Framework (SIFP) comes into play – a framework to do interface-checking (as opposed to the oft-pernicious type checking). It serves mainly two purposes:

  • Check if an object “can be used”.
  • Give a limited form of design by contract: Code using (our notion of) interfaces makes explicit what kinds of input it expects.

What do we understand by “An object is safe to use?” As exemplified by the snippet above we mean essentially three things:

  • The object has the attribute we need.
  • The attribute can be used in the way we need, e.g. Is it callable or not?
  • In case it’s callable does it have the signature we expect/know?

The above implementation “solves” the first two items, but not the third. Solving it would involve some hackery with the module inspect that at this time is beyond me, so I just leave it quiet for now.

Due to the incredible introspective capabilities of Python there is really no need, IMHO, to add interface-implementation declarations to classes. The public interface exposed by a class is in the code itself and can be probed by introspection. Such would-be declarations would also suffer from two major drawbacks: Python objects are very dynamic and can grow new methods or loose old ones during their lifetime, i.e. their interface can change. Runtime interface changes are probably very foolish, but they are allowed anyway, and to quote W. Blake “If the fool would persist in his folly he would become wise.” A second reason is that there is a lot – some would say all – of Python code out there that doesn’t know of SIFP so we would have to implement runtime interface checking anyway for SIFP to be of any use.

Another worthy bit is that interfaces are just plain Python classes, albeit with Interface metaclass, and interface extension is just class inheritance. In particular you can add code bodies in the interface declaration – I do it happily, and then my interfaces also work like mixins.

Another, and totally different thing, is the semantic issue. In the test code we have implemented an IStack interface – every programmer has an intuitive idea of what a stack is and what is expected of it. But these semantic issues cannot be automatically tested by any conceivable program we could write, only documentation and standardization can settle them. There is nothing stopping a programmer to, say, raise a ZeroDivisionError in the pop method. If he does, he should rot in programmer’s hell, but if we were to rule out that possibility we would have to call the pop method, which is a no-no since it can alter the object’s state.

Known caveats: Besides not checking the signatures (and this can trip you up as exemplified in the test code) interface names may shadow the metaclass methods via directly declaring them or by inheriting from some class (not necessarily an interface) that does so. We could add a check to __init__ that crawled through the bases of the interface to catch any offending attributes (or declare the metaclass methods by special methods?) but we chose to keep things simple.

Have yourselves a merry Pythonic day, G. Rodrigues

(*) Protocol is probably a better name. And then again, it’s not as catchy.

Created by Gonçalo Rodrigues on Sat, 30 Nov 2002 (PSF)
Python recipes (4591)
Gonçalo Rodrigues's recipes (9)
Rapid prototyping (11)

Required Modules

  • (none specified)

Other Information and Tasks