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:
- The definition of a way to declare abstract methods, and
- 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.
