Is there a consensus on how to check a polymorphic instance?
Steven Bethard
steven.bethard at gmail.com
Tue Nov 23 03:15:13 EST 2004
Mike Meng wrote:
> Here is my problem. My project needs to parse some user input
> string according to specific rules. The rules can be defined with a
> string, a CSV file, an XML file, and other forms. Followed the standard
> OO style, I designed a dictionary-like base class to abstract those
> different sources and provides other parts a uniform interface to query
> those rules. I put some hard code into the base class and expect user
> to inherit from it.
The big question, I guess, is what do you want to happen if a user did
not inherit from your base class, but still provides all the appropriate
functionality? In a dynamically-typed language like Python the answer
is usually that the user's class should still be considered valid,
though of course there are exceptions to this rule.
For example, say I write the following function:
>>> def process_rules(rules):
... for rule in rules:
... print rule.name
...
Do I really care if the 'rules' object inherits from, say, the Rules
class? Probably not. I care that it is iterable, and that the items
that it contains have a name attribute. Here's a few different ways I
could write an object that conforms to this interface:
>>> class Rule(object):
... def __init__(self, name):
... self.name = name
...
>>> process_rules([Rule(s) for s in 'abc'])
a
b
c
>>> class Rules(object):
... def __init__(self, names):
... self.names = names
... def __iter__(self):
... for name in self.names:
... yield Rule(name)
...
>>> process_rules(Rules('abc'))
a
b
c
>>> class NastyRules(object):
... def __init__(self, names):
... self.names = names
... self.index = -1
... def __iter__(self):
... return iter([self]*len(self.names))
... def __getattr__(self, attr):
... if attr == 'name':
... self.index += 1
... return self.names[self.index]
... raise AttributeError
...
>>> process_rules(NastyRules('abc'))
a
b
c
Of course, if you write your code like NastyRules, you should be drug
out into the street and shot ;) but the point is that there are a
variety of ways that a class could support the given interface and still
do everything you've asked it to. If you'd like to give your users the
option to implement the interface in whatever way seems most
appropriate, you shouldn't be testing isinstance:
>>> isinstance([Rule(s) for s in 'abc'], list)
True
>>> isinstance(Rules('abc'), list)
False
>>> isinstance(NastyRules('abc'), list)
False
In the example above, if you test isinstance, you disallow your user
from writing either of the other two implementations.
Note that you can still give useful error messages if you catch the
appropriate exceptions:
>>> def process_rules(rules):
... try:
... rules = iter(rules)
... except TypeError:
... raise TypeError('process_rules argument must '
... 'support iterator protocol')
... for rule in rules:
... try:
... print rule.name
... except AttributeError:
... raise TypeError('process_rules argument must '
... 'produce objects with name attribute')
...
>>> process_rules([Rule(s) for s in 'abc'])
a
b
c
>>> process_rules(Rules('abc'))
a
b
c
>>> process_rules(NastyRules('abc'))
a
b
c
>>> process_rules(1)
Traceback (most recent call last):
File "<interactive input>", line 1, in ?
File "<interactive input>", line 5, in process_rules
TypeError: process_rules argument must support iterator protocol
>>> process_rules(range(10))
Traceback (most recent call last):
File "<interactive input>", line 1, in ?
File "<interactive input>", line 11, in process_rules
TypeError: process_rules argument must produce objects with name attribute
So again, the real question here is: If a user creates a class that
appropriately implements the interface, but doesn't inherit from your
base class, should their code fail?
Steve
More information about the Python-list
mailing list