Style for overwritten methods of abstract classes

Alex Martelli aleax at aleax.it
Fri Jan 4 11:49:25 EST 2002


"Stefan Schwarzer" <s.schwarzer at ndh.net> wrote in message
news:3C34A6AE.4293E82C at ndh.net...
> Hello all :)
>
> As an example, I have an abstract base class CacheBase which provides
> an interface to a persistent storage. All methods _could_ be either
> overwritten (__init__, __del__) or _have to_ be provided (__getitem__,
> __setitem__). Initially, I wrote
    ...
> So I can think of three different approaches:
> 1. write the code as above
> 2. merely describe the implementation/interfaces (e. g. in the class
>    docstring or in a comment); write no code
> 3. use code but let the abstract methods raise NotImplementedError
>    instead of providing a default behaviour which might be useless
> (4. Are there others?)

Yes, there are.  You could have a metaclass that does ensure the
behavior you desire, and specify specialmethods that MAY or MUST
be provided in class-attributes.

It's easier in 2.2, of course, where you can make your metaclass
just by inheriting from type and tweaking it slightly, so, for
example, here's a metaclass to implement "must-checking", only:


class AbstractionError(Exception): pass

class me(type):
    def __init__(self, name, bases, classdict):
        type.__init__(self, name, bases, classdict)
        if bases == ():
            missing = self.__must_methods =
classdict.get('_must_methods_',())
        else:
            missing = []
            for x in self.__must_methods:
                if not hasattr(self, x): missing.append(x)
        if missing:
            errmsg = "Can't instantiate (%s), missing methods (%s)" % (
                name, ' '.join(missing))
            class dontcallme(me):
                def __call__(*args, **kwds):
                    raise AbstractionError, errmsg
            self.__class__ = dontcallme
        else:
            class callme(me):
                __call__ = type.__call__
            self.__class__ = callme

def tryMaking(klass):
    try: x = klass()
    except AbstractionError, noway:
        print noway
    else:
        print "made", x

class AbstractBase:
    __metaclass__ = me
    _must_methods_ = '__getitem__ __setitem__'.split()
tryMaking(AbstractBase)

class SemiConcrete(AbstractBase):
    def __getitem__(self, index): pass
tryMaking(SemiConcrete)

class ConcreteClass(SemiConcrete):
    def __setitem__(self, index, value): pass
tryMaking(ConcreteClass)


Of course, you may choose to be even more thorough in your checks, e.g.,
check for compatibility of signature rather than just for equal names.
Adding the "may" (raising errors if a class using me as its meta ever
dares define a class-attribute not in an allowed-list) is left as an
exercise to the reader -- it's not hard, of course.

The (tiny) advantage of this approach, just like for any "declaration",
is that you get your error at the earliest possible moment -- when a
class that doesn't define all methods you "know" it MUST define (thus,
an abstract class) is *instantiated*.  If you didn't want to allow partial
definition such as in SemiConcrete, you could get the error at the time
such a class is *defined*, of course.

You pay the price in terms of runtime only when a class is defined --
that's when me.__init__ runs.  The larger price is exactly in the
extra checks that this approach lets you do...

That price is hefty indeed.  Now, suddenly, even if you never use nor
need (e.g.) __getitem__, it becomes an error not to have it: you're
not within Python's wonderfully-smooth world of signature based
polymorphism anymore.


But, to each its own.  Python gives you all the power you need to
shoot yourself in the foot, in this and several other ways.  While
still not the best language for "Bondage and Discipline Programming",
Python does enjoy the advantage (?) that you may easily devise and
implement your own cruel and unusual punishments for yourself
along these lines.


Alex






More information about the Python-list mailing list