Navigation

home code debian images resume weblog wiki

Older news:

Dec 11, 2006:

Debian GNU/Linux

About lychnis.net
Jul 7, 2005:
Wanneer gebruik je -d en wanneer gebruik je -t?
Feb 18, 2005:
Mixing whitespace
Jan 10, 2005:
The difference between dogs and cats
Dec 22, 2004:
Sunrise in winter
Dec 12, 2004:
New site layout

Browse:


Abstract methods in python (3)/programming

Posted on 2004-01-22 by ivo :: /programming :: link

Ok, here is the same code as in the last two articles, this time with more explanation. The point is that python doesn't have a notion of “abstract methods.” Abstract methods are part of an base class that defines an interface, without any code. Abstract methods can't be called directly, because they don't contain any code in their definition.

In the definition of the base class, you may want to include a specific method that is part of the interface, but the specific implementation is still unknown. A popular example seems to be the drawing of a point or a line in a graphical application.

The classes Point and Line share several implementation details, but differ on other. In particular, the way they are drawn is completely different (you will want to optimize the drawing of a line). Suppose these two classes are derived from the same class, Object. It is possible to separate the implementation of the method draw of these two classes, while draw can still be called from the base class Object.

The text below will introduce some utility classes that make this possible.

The goal of this article is defining a way to make it possible to define classes such as the following (not yet paying attention to the proper syntax):

class Object (object):
    abstract draw()

    def update(self):
        self.draw()

class Point (Object):
    def draw(self):
        ...
    ...

class Line (Object):
    def draw(self):
        ...
    ...

The method draw of the class Object cannot be implemented, because the concept of ‘drawing’ a generic object is undefined. Other methods, such as the update above may want to use draw anyway, because it is part of the specification for the Object class and its descendants.

The implementation in python exists of two parts:

  1. The definition of a way to declare abstract methods, and
  2. a way to restrict the creation/usage of these abstract classes.

First the declaration part. To declare an abstract method, we can use callable class variables:

class Object (object):
    draw = AbstractMethod()

When somebody tries to call Object.draw(), an exception will be raised. But as long as methods in Object use self.draw(), they will actually use Point.draw(), because self will be of type Point.

If AbstractMethod is a class, draw will be an instance of this class, so we can make draw callable, and raise a proper exception (TypeError or NotImplementedError for example) if it is called instead of an implementation in one of the descendant classes.

class AbstractMethod (object):
    def __init__(self, func):
        self._function = func

    def __get__(self, obj, type):
        return self.AbstractMethodHelper(self._function, type)

    class AbstractMethodHelper (object):
        def __init__(self, func, cls):
            self._function = func
            self._class = cls

        def __call__(self, *args, **kwargs):
            raise TypeError('Abstract method `' + self._class.__name__ \
                            + '.' + self._function + '\' called')

So now we can declare Object as follows:

class Object (object):
    draw = AbstractMethod('draw')
    def update(self):
        self.draw()

If we tried to call Object().draw() directly, we get an exception:

>>> Object().draw()
TypeError: Abstract method `Object.draw' called

The same happens with Object().update():

>>> Object().update()
TypeError: Abstract method `Object.draw' called

If we implement a descendant class which implements draw, there is no error.

class Point (Object):
    def draw(self):
        print 'Point.draw called'

(Note that there is no definition for update in Point, it uses the implementation inherited from Object.)

>>> Point().update()
Point.draw called

Of course, we shouldn't be getting an exception at all if we try to call an abstract function. It should be impossible to create an instance of a class that has one or more abstract methods in its definition (either declared directly in the class definition, or implicitly via inheritance without overriding it with a real method). We can solve this pretty easily by declaring a metaclass that checks if there are any abstract methods in a class definition, and raise an exception if there are.

class Metaclass (type):
    def __init__(cls, name, bases, *args, **kwargs):
        type.__init__(cls, name, bases, *args, **kwargs)
        cls.__new__ = staticmethod(cls.new)

        ancestors = list(cls.__mro__)
        ancestors.reverse()  # Start with __builtin__.object
        for ancestor in ancestors:
            for clsname, clst in ancestor.__dict__.items():
                if isinstance(clst, AbstractMethod):
                    abstractmethods.append(clsname)
                else:
                    if clsname in abstractmethods:
                        abstractmethods.remove(clsname)

        abstractmethods.sort()
        setattr(cls, '__abstractmethods__', abstractmethods)

    def new(self, cls):
        if len(cls.__abstractmethods__):
            raise NotImplementedError('Can\'t instantiate class `' + \
                                      cls.__name__ + '\';\n' + \
                                      'Abstract methods: ' + \
                                      ", ".join(cls.__abstractmethods__))

        return object.__new__(self)

The definition of Object becomes:

class Object (object):
    __metaclass__ = Metaclass
    draw = AbstractMethod('draw')

This has the final result:

>>> Point().update()
Point.draw called
>>> Object().update()
NotImplementedError: Can't instantiate class `Object';
Abstract methods: draw

The error can be caught much earlier on when the exception is raised when the class is instantiated.

There is one remaining issue, which is that descendant classes of Object which don't implement all the abstract methods defined in Object can also not be instantiated:

>>> class FooClass (Object):
...     pass
>>> FooClass()
NotImplementedError: Can't instantiate class `FooClass';
Abstract methods: draw

The code in the last article didn't do this, but the code in this article checks all ancestors for any abstract methods that haven't been implemented.