How about "pure virtual methods"?

George Sakkis gsakkis at rutgers.edu
Thu Jan 6 18:45:51 CET 2005


"Noam Raphael" <noamr at remove.the.dot.myrea.lbox.com> wrote:

> Thanks for your suggestion, but it has several problems which the added
> class solves:
>
> * This is a very long code just to write "you must implement this
> method". Having a standard way to say that is better.
> * You can instantiate the base class, which doesn't make sense.
> * You must use testing to check whether a concrete class which you
> derived from the base class really implemented all the abstract methods.
> Testing is a good thing, but it seems to me that when the code specifies
> exactly what should happen, and it doesn't make sense for it not to
> happen, there's no point in having a separate test for it.


Here's a more refined implementation of the one posted before that
addresses these issues. It defines 'abstractclass', which would be
a nice class decoraror when (and if) class decorators make it into
the language. The extra benefit compared to the MustImplement metaclass
is that it allows a class with no abstract methods to be defined as abstract.
This makes no sense if one defines an abstract class as "a class with one
or more abstract (deferred) methods". Perhaps a more useful and general
definition of an abstract class is "a class that is not allowed to be instantiated".
Deferred methods are one reason for making a class uninstantiable, but it's
not the only one. Sometimes it is useful to define a base class that provides
a default implementation of a full protocol, yet disallow its direct instantiation,
as in the example below.

George

#===========================================================
# test_abstract.py

import unittest
from abstract import abstractclass

class AbstractTestCase(unittest.TestCase):

    def test_no_abstractmethods(self):
        class SomeBase(object):
            # This class has no abstract methods; yet calling foo() or bar()
            # on an instance of this class would cause infinite recursion.
            # Hence it is defined as abstract, and its concrete subclasses
            # should override at least one of (foo,bar) in a way that breaks
            # the recursion.
            def __init__(self, x):
                self._x = x
            def foo(self, y):
                return self.bar(self._x + y)
            def bar(self, y):
                return self.foo(self._x - y)
        SomeBase = abstractclass(SomeBase)

        class Derived(SomeBase):
            def __init__(self,x):
                SomeBase.__init__(self,x)
            def foo(self,y):
                return self._x * y

        self.assertRaises(NotImplementedError, SomeBase, 5)
        self.assertEquals(Derived(5).bar(2), 15)

if __name__ == '__main__':
    unittest.main()

#===========================================================
# abstract.py

import inspect

__all__ = ["abstractclass", "abstractmethod", "AbstractCheckMeta"]


def abstractclass(cls):
    '''Make a class abstract.

    Example::
        # hopefully class decorators will be supported in python 2.x
        # for some x, x>4
        #@abstractclass
        class SomeBase(object):
            @abstractmethod
            def function(self):
                """Implement me"""
        # the only way as of python 2.4
        SomeBase = abstractclass(SomeBase)

    @param cls: A new-style class object.
    @return: A surrogate of C{cls} that behaves as abstract. The returned
        class raises NotImplementedError if attempted to be instantiated
        directly; still its subclasses may call its __init__. A subclass of
        the returned class is also abstract if it has one or more abstract
        methods, or if it is also explicitly decorated by this function. A
        method is declared abstract by being assigned to NotImplemented (or
        decorated by L{abstractmethod}).
    @raise TypeError: If there is a metaclass conflict between C{type(cls)}
        and L{AbstractCheckMeta}, or if C{cls} has an C{__abstractmethods__}
        attribute.
    '''
    # check if cls has AbstractCheckMeta (or a subtype) for metaclass
    metaclass = type(cls)
    if not issubclass(metaclass, AbstractCheckMeta):
        # it doesn't; try to make AbstractCheckMeta its metaclass by
        # inheriting from _AbstractCheck
        cls = metaclass(cls.__name__, (_AbstractCheck,) + cls.__bases__,
                        dict(cls.__dict__))
    # replace __init__ with a proxy ensuring that __init__ is called by a
    # subclass (but not directly)
    old_init = getattr(cls,'__init__',None)
    def new_init(self,*args,**kwds):
        if self.__class__ is cls:
            raise NotImplementedError("%s is an abstract class" % cls.__name__)
        if old_init is not None:
            old_init(self,*args,**kwds)
    setattr(cls,'__init__',new_init)
    return cls


def abstractmethod(function):
    '''A method decorator for those who prefer the parameters declared.'''
    return NotImplemented


class AbstractCheckMeta(type):
    '''A metaclass to detect instantiation of abstract classes.'''

    def __init__(cls, name, bases, dict):
        if '__abstractmethods__' in cls.__dict__:
            raise TypeError("'__abstractmethods__' is already defined in "
                            "class '%s': %s" % (cls.__name__,
                            cls.__dict__['__abstractmethods__']))
        type.__init__(cls, name, bases, dict)
        cls.__abstractmethods__ = [name for name, value in
                                   inspect.getmembers(cls)
                                   if value is NotImplemented]

    def __call__(cls, *args, **kwargs):
        if cls.__abstractmethods__:
            raise NotImplementedError(
                "Class '%s' cannot be instantiated: Methods %s are abstract."
                % (cls.__name__,", ".join(map(repr,cls.__abstractmethods__))))
        return type.__call__(cls, *args, **kwargs)


class _AbstractCheck(object):
    '''
    A class to stick anywhere in an inheritance chain to make its
    descendants being checked for whether they are abstract.
    '''
    __metaclass__ = AbstractCheckMeta





More information about the Python-list mailing list