Python Coding Style – Pythonic Use of the isinstance Function

coding-stylepython

Whenever I find myself wanting to use the isinstance() function I usually know that I'm doing something wrong and end up changing my ways. However, in this case I think I have a valid use for it. I will use shapes to illustrate my point although I am not actually working with shapes. I am parsing XML configuration files that look like the following:

<square>
  <width>7</width>
</square>
<rectangle>
  <width>5</width>
  <height>7</height>
</rectangle>
<circle>
  <radius>4</radius>
</circle>

For each element I create an instance of the Shape class and build up a list of Shape objects in a class called the ShapeContainer. Different parts of the rest of my application need to refer to the ShapeContainer to get certain shapes. Depending on what the code is doing it might need just rectangles, or it might operate on all quadrangles, or it might operate on all shapes. I have created the following function in the ShapeContainer class (the actual function uses a list comprehension but I have expanded it here for readability):

def locate(self, shapeClass):
  result = []
  for shape in self.__shapes:
    if isinstance(shape,shapeClass):
      result.append(shape)
  return result

Is this a valid use of the isinstance function? Is there another way I can do this which might be more pythonic?

Best Answer

Use polymorphism and duck-typing before isinstance()

You generally define what you want to do with the shapes, then either use polymorphism to adjust how each shape responds to what you want to do, or you use duck typing; test if the shape at hand can do the thing you want to do in the first place. This is the invocation versus introspection trade-off, conventional wisdom states that invocation is preferable over introspection, but in Python, duck-typing is preferred over isinstance testing.

So you need to work out why you need to filter on class in the first place; why do you need to find all the rectangles? Perhaps they are the only type that implements a certain method, in which case you use duck typing to test for that method instead.

Or perhaps you need to process each type in a different way, because each type has to be handled just right for whatever you are doing with them. In that case, you need to give each shape the same method, but implement it differently for each type, placing the responsibility for adaptation in each object.

ABCs

With duck-typing, you may find that you have to test for multiple methods, and thus a isinstance() test may look a better option. In such cases, using a Abstract Base Class (ABC) could also be an option; using an ABC let's you 'paint' several different classes as being the right type for a given operation, for example. Using a ABC let's you focus on the tasks that need to be performed rather than the specific implementations used; you can have a Paintable ABC, a Printable ABC, etc.

Zope interfaces and component architecture

If you find your application is using an awful lot of ABCs or you keep having to add polymorphic methods to your classes to deal with various different situations, the next step is to consider using a full-blown component architecture, such as the Zope Component Architecture (ZCA).

zope.interface interfaces are ABCs on steroids, especially when combined with the ZCA adapters. Interfaces document expected behaviour of a class:

if IRectangleShape.providedBy(yourshape):
    # it's a rectangle

but it also let's you look up adapters; instead of putting all the behaviours for every use of shapes in your shape classes, you implement adapters to provide polymorphic behaviours for specific use-cases. You can adapt your shapes to be printable, or paintable, or exportable to XML:

class RectangleXMLExport(object):
    adapts(IRectangleShape)
    provides(IXMLExport)

    def __init__(self, rectangle):
        self.rectangle = rectangle

    def export(self):
        return u'<rectangle><width>{0}</width><height>{0}</height></rectangle>'.format(
            self.rectangle.width, self.rectangle.height)

and your code merely has to look up adapters for each shape:

for shape in shapebag:
    self.result.append(IXMLExport(shape).export())
Related Topic