protocols, inheritance and polymorphism

Steven Bethard steven.bethard at gmail.com
Tue Nov 23 19:02:32 CET 2004


Dan Perl wrote:
> I have a question here, as we are discussing now protocol interfaces vs. 
> inheritance.  Is using a class that implements a protocol without inheriting 
> from a base class still "polymorphism"?

Some good definitions:

http://en.wikipedia.org/wiki/Polymorphism_%28computer_science%29)

There are probably two types of polymorphism relevant to the current 
discussion: parametric polymorphism and subtyping polymorphism.  In 
parametric polymorphism, a function is written so as to work with 
objects of different types.  Subtyping polymorphism is a subtype of 
parametric polymorphism that restricts the polymorphism to only objects 
that inherit from a specified base class.

In its most basic use, Python is inherently parametrically polymorphic 
-- all functions are written so as to work with different types. 
Consider a few simple functions:

 >>> def f(x):
...     return 'f(%s)' % x
...
 >>> def g(x):
... 	return 2**x
...

Both of these functions work perfectly well with a wide range of objects:

 >>> f(1), f(2.0), f('abc'), f([1,2,3])
('f(1)', 'f(2.0)', 'f(abc)', 'f([1, 2, 3])')
 >>> g(4), g(5.0), g(decimal.Decimal(6))
(16, 32.0, Decimal("64"))

I didn't have to do anything special to make them work this way.  Any 
object that supports __str__ can be passed to f, e.g.:


 >>> class S(object):
...     def __str__(self):
...         return 'str'
...
 >>> f(S())
'f(str)'

and any object that supports __rpow__ (or __coerce__, or some other way 
to be raised to a power) can be passed to g, e.g.:

 >>> class P(object):
...     def __rpow__(self, other):
...         return other**other
...
 >>> g(P())
4

So both functions f and g are parametrically polymorphic.  If you wanted 
to artifically restrict f or g to support only subtype polymorphism, you 
could do this with isinstance.  In most cases, I would probably advise 
against this -- if you're using isinstance to restrict your functions to 
only subtype polymorphism, you're reducing the usefulness of your code.

This is not to say that there are never any times when you want to use 
isinstance.  Here's a good example from my code for a Bunch class[1]:

class Bunch(object):
     ...
     def update(self, *args, **kwds):
         if len(args) == 1:
             other, = args
             if isinstance(other, self.__class__):
                 other = other.__dict__
             try:
                 self.__dict__.update(other)
             except TypeError:
                 raise TypeError('cannot update Bunch with %s' %
                                 type(other).__name__)
         elif len(args) != 0:
             raise TypeError('expected 1 argument, got %i' % len(args))
         self.__dict__.update(kwds)

In the code here, we use isinstance to check on whether or not to use 
the argument's __dict__.  Now I could have written the isinstance 
section as:

             try:
                 other = other.__dict__
             except AttributeError:
                 pass

and then the function would 'work' with any object that had a __dict__ 
attribute.  But since other classes are not guaranteed to use __dict__ 
in the same way that Bunch does (e.g. they're not guaranteed to have all 
attributes in their __dict__s), this would give some potentially 
confusing results:

 >>> class C(object):
...     def x():
...         def get(self):
...             return 5
...         return dict(fget=get)
...     x = property(**x())
...
 >>> c = C()
 >>> b = Bunch()
 >>> b.update(c)
 >>> b.x
Traceback (most recent call last):
   File "<interactive input>", line 1, in ?
AttributeError: 'Bunch' object has no attribute 'x'

The point here is that I'm using isinstance here because I *know* there 
are objects out there that, unlike Bunch objects, may have 'attributes' 
not stored in their __dict__, and I *know* they might get passed to 
Bunch.update.  Since I can't appropriately support their use, I want the 
user to be notified of this (with the TypeError).

Of course, I'm still making heavy use of the more general parametric 
polymorphism because even if the object doesn't inherit from Bunch, I 
still pass it to dict.update, which works with any type that supports 
the mapping protocol (and a few other types besides).  So if, in the 
future dict.update gains support for some new type, my code doesn't 
artifically restrict the type of parameter that can be passed to my 
function.

Hope this was helpful (or at least vaguely intelligible) ;)

STeve


[1] Thanks to Peter Otten for the implementation suggestion



More information about the Python-list mailing list