protocols, inheritance and polymorphism
Steven Bethard
steven.bethard at gmail.com
Tue Nov 23 13:02:32 EST 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