Tuesday, April 12, 2011

Easy as ABCMeta

My earlier attempt at explaining abstract classes in Python didn't take into account the abc module, used for creating abstract base classes.  What follows is an example I find useful for using the abc module to define virtual subclasses.  A virtual subclass is different from a subclass in the traditional sense.  Normally, a subclass is something that inherits from a base class.  In Python, we can use this hierarchical structure to implement abstract base classes that do nothing, or raise a NotImplementedError exception.  It is up to the subclasses to provide the implementation.

Virtual subclasses are different in that there is no inheritance - the abstract base class registers virtual subclasses.  This registration takes place outside of the class hierarchy, so its easy to replace virtual subclasses, or remove them entirely.  This is all done with the ABCMeta class, part of the abc module.

Suppose I have a Shape class that can move to a specified point.  Such an implementation might look like this.

class Shape(object):
    
    def move(self, x, y):
        
        print 'Moving to X: %s Y: %s'%(x, y)
        
if __name__ == '__main__':
    
    my_shape = Shape()
    
    my_point = (50, 100)
    
    my_shape.move(*my_point)

The Shape.move() method is expecting a point in which to move.  This is passed as a tuple, containing x, y coordinates.  However, Shape is a legacy class that's been around forever and is being replaced with ShapeNew.

class ShapeNew(object):
    
    def move(self, point):
        
        print 'New Moving to X: %s Y: %s'%(point.x, point.y)

The ShapeNew.move() now expects a point object with x and y attributes, not a tuple with zero-based index lookups.  This is where the ABCMeta class comes in handy.

from abc import ABCMeta

class Point(object):
    
    __metaclass__ = ABCMeta
    
    def __init__(self, *args):
        
        self.data = args
        
    def __getitem__(self, index):
        
        return self.data[index]
        
    def __len__(self):
        
        return len(self.data)
        
    def get_x(self):
        
        return self[0]
        
    def get_y(self):
        
        return self[1]
        
    x = property(get_x)
    y = property(get_y)        
        
Point.register(tuple)

We now have a Point class that we can use with the ShapeNew.move() method.  You'll notice, Point sets its __metaclass__ attribute to ABCMeta.  Following the definition of Point, we see Point.register(tuple).  This tells the Python that tuple is a virtual subclass of Point.  So Point can now be used anywhere tuples were used for the old Shape.move() method.

from abc import ABCMeta

class Point(object):
    
    __metaclass__ = ABCMeta
    
    def __init__(self, *args):
        
        self.data = args
        
    def __getitem__(self, index):
        
        return self.data[index]
        
    def __len__(self):
        
        return len(self.data)
        
    def get_x(self):
        
        return self[0]
        
    def get_y(self):
        
        return self[1]
        
    x = property(get_x)
    y = property(get_y)        
        
Point.register(tuple)

class Shape(object):
    
    def move(self, x, y):
        
        print 'Moving to X: %s Y: %s'%(x, y)
        
class ShapeNew(object):
    
    def move(self, point):
        
        print 'New Moving to X: %s Y: %s'%(point.x, point.y)
    
if __name__ == '__main__':
    
    my_shape = Shape()
    my_shape_new = ShapeNew()
    
    my_point = Point(50, 100)
    
    my_shape.move(*my_point)
    my_shape_new.move(my_point)

Here, we're using ABCMeta to help us transition from legacy code to a newer implementation.  So when Point no longer needs to be a tuple, we can remove ABCMeta as its __metaclass__ and remove the Point.register(tuple) resgistration.

3 comments :

  1. The registration makes issubclass(tuple, Point) and isinstance((50, 100), Point) both return True, but your example does not make use of this. Your example still runs perfectly, if you remove ABCMeta as its __metaclass__ and remove the Point.register(tuple) registration without changing anything else.

    ReplyDelete
  2. Naveen Michaud-AgrawalNovember 4, 2011 at 9:54 PM

    Wait, does Anonymous's comment above mean that you can accomplish this migration with ABCmeta?

    ReplyDelete