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

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.

Python, 112 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
class AbstractMethod (object):
    """Defines a class to create abstract methods

    @example:
        class Foo:
            foo = AbstractMethod('foo')
    """
    def __init__(self, func):
        """Constructor

        @params func: name of the function (used when raising an
            exception).
        @type func: str
        """
        self._function = func

    def __get__(self, obj, type):
        """Get callable object

        @returns An instance of AbstractMethodHelper.

        This trickery is needed to get the name of the class for which
        an abstract method was requested, otherwise it would be
        sufficient to include a __call__ method in the AbstractMethod
        class itself.
        """
        return self.AbstractMethodHelper(self._function, type)

    class AbstractMethodHelper (object):
        """Abstract method helper class

        An AbstractMethodHelper instance is a callable object that
        represents an abstract method.
        """
        def __init__(self, func, cls):
            self._function = func
            self._class = cls

        def __call__(self, *args, **kwargs):
            """Call abstract method

            Raises a TypeError, because abstract methods can not be
            called.
            """
            raise TypeError('Abstract method `' + self._class.__name__ \
                            + '.' + self._function + '\' called')


class Metaclass (type):
    def __init__(cls, name, bases, *args, **kwargs):
        """Configure a new class

        @param cls: Class object
        @param name: Name of the class
        @param bases: All base classes for cls
        """
        super(Metaclass, cls).__init__(cls, name, bases, *args, **kwargs)

        # Detach cls.new() from class Metaclass, and make it a method
        # of cls.
        cls.__new__ = staticmethod(cls.new)

        # Find all abstract methods, and assign the resulting list to
        # cls.__abstractmethods__, so we can read that variable when a
        # request for allocation (__new__) is done.
        abstractmethods = []
        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):
        """Allocator for class cls

        @param self: Class object for which an instance should be
            created.

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

        return object.__new__(self)


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

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


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

A bit more explanation about why some things had to be done is in the article I originally wrote to introduce this code: http://www.lychnis.net/index/programming/python-abstract-methods-3.lychnis

4 comments

Alain Pointdexter 20 years, 2 months ago  # | flag

I don't understand. I'm a Python newbie; excuse me if don't get it right but i thought Python was about simplicity ...

What's wrong with:

def abstract_method(self):

raise 'Abstract method, please override'
Ivo Timmermans (author) 20 years, 2 months ago  # | flag

Complexity. For one thing: clarity. My version gives the exact class and method names where the abstract method was defined.

Second: certainty. With that metaclass, it is guaranteed that you will never accitentally instantiate a class with one or more abstract classes.

Gustavo Dagostin 18 years, 5 months ago  # | flag

Constructor of subclasses requires 2 arguments. I have created an abstract class which (besides self) has one argument on the __init__ method.

Later, I've created a subclass of that abstract one, which added one more parameter to the __init__.

When creating an instance of the later, the error below pops up:

Traceback (most recent call last):
  File "ConcreteClass.py", line 70, in ?
    cc = ConcreteClass(param1, param2)
TypeError: new() takes exactly 2 arguments (4 given)

Bug?

Am I restricted to __init__(self) only (no other parameters)?

Brandon Pedersen 15 years, 8 months ago  # | flag

Nice, while it is not amazingly clean it is great if you don't want somebody to instantiate an abstract class...This is probably the simplest recipe for creating an abstract class I have seen so far...can't wait for python 3000 when interfaces and abstract stuff is supported natively!!!

By the way if you want to have parameters in the __init__ of sub classes you just need to change line 80 for new to

def new(self, cls, *args, **kwargs)

Also, I noticed that you really don't need to pass the name of the function to instantiate AbstractMethod...maybe it was needed in previous versions of python but 2.5 works without it. I changed the __init__ for AbstractMethod to have func=None. That way in my abstract class I just do

an_abstract_method = AbstractMethod()

that way it is not as redundant with passing the name again to the AbstractMethod. It still spits out the name when it says that I can't instantiate a class with abstract methods.

Created by Ivo Timmermans on Fri, 23 Jan 2004 (PSF)
Python recipes (4591)
Ivo Timmermans's recipes (1)

Required Modules

  • (none specified)

Other Information and Tasks