isinstance() necessary and helpful, sometimes

Raymond Hettinger othello at
Fri Jan 25 13:10:54 EST 2002

"Alex Martelli" <aleax at> wrote in message
news:a2r97q$dpi$1 at
> > 3.  Adding functionality with a Decorator pattern, it's easy to end up
> > re-wrapping an object more than once.  To avoid multiple wraps, it's
> helpful
> > to use isinstance() to see if the argument is already of the right type.
> In
> > my code, the Table class added elementwise operations to built-in lists.
> > Everywhere I needed a Table, the user could have easily supplied either
> > Table or a List, so used something like:   arg = isinstance(arg,Table)
> > arg or Table(arg).
> If you _add_ functionality, hasattr seems just about right.  If you
> _modify_ functionality, so that the Decorated object has no extra
> attributes wrt the unDecorated ones, then that doesn't help -- and
> if you do all your Decoration with [subclasses of] the same Decorator
> class, then typetesting for that is reasonable here.
> Choosing typetesting in this case may impede framework augmentation
> "from the outside".  If client-code needs to provide some kind of
> SuperTable, it must subclass it from Table, which may mean carrying
> around unwanted baggage.  One approach (to keep typetesting) is to
> make Table into a no-extra-baggage abstract class -- either a pure
> interface, or an interface plus some helpful Template methods, or
> a full-fledged Haskell-like typeclass, but still without possibly
> unwanted data attributes.  Another possibility might be to add to
> Table an attribute (_yesiamalreadydecoratedthankyouverymuch would
> seem to be the natural name for it) and replace the typechecking
> with a check for that attribute: this still lets client-code choose
> to subclass Table, but offers client-code the extra possibility of
> reimplementing its own decorators from scratch -- it just has to
> supply that 'flag' attribute as well to assert it knows what it's
> doing.  Admittedly, such a measure of generality may be excessive
> and unwarranted, but it's a possibility to keep in mind when a
> framework of this kind is evolving -- even if you strictly relied
> on typetesting in version N, you can provide backwards-compatible
> greated flexibility in version N+1 by switching to using a flag
> attribute instead.

This is a tough design decision.   The considerations are:
1. It would be great to keep open the possibility of a SuperTable.
2. hasattr() bugs me because I want to ensure that all of the expected
    methods are available not just one.  Also, passing the attribute name
    as a string has a bad feel to it though I can't say why.
3. except AttributeError runs the risk of trapping real errors.  It is too
    all inclusive to be used safely as a means of making sure an object
    has the required interface.
4. Of all of the uses of type testing, having a class be able to recognize
    one of its own is amoung the least aggregious.
5. For all its faults, isinstance() has the virtue of clarity.  Anyone
    myself) reading my code and finding isinstance(x,Table) will more
    readily grasp intent of the code (preventing double decoration) than
    would with hasattr() or except AttributeError.

Executive Summary:  It would be darned nice if there were a straightforward
way of assuring a that a particular interface were available but not require
particular object class.

> > 4.  Some code needs to fork paths when the class indicates an intrinsic
> > quality not revealed by the attributes.  Implementing matrix
> exponentation,
> > __pow__(self,exponent), efficiently requires testing
> > isinstance(exponent,int) because the float and complex cases are to be
> > computed differently.
> Wouldn't we want in this case to treat X**1.0 the same as X**1 the
> same as X**1L...?  A test such as exponent==int(exponent) would
> then seem to be more general and useful than isinstance(exponent,int).

Good idea, I made the change!  This avoids an explicit type test while
recognizing that integers do have special properties and sometimes you
need to test for being integerlikeness.

> As a quibble, this case is often better handled by try/except rather
> than if/else -- and I don't think that squashing the test into one
> expression is worth it.  Take exactly the example you suggest:
> def myConj(z): return hasattr(z,'conjugate') and z.conjugate() or z
> this only works because, if z.conjugate() is false, then z==z.conjugate().
> When generalized, e.g., to .real, it breaks down:
> >>> def myReal(z): return hasattr(z,'real') and z.real or z
> ...
> >>> print myReal(0+4j)
> 4j
> "Oops".  An if/else or try/except doesn't run such risks:

I made this change right away.  The and/or style is a bug!
Try/except or hasattr() both leave the interface open for UserComplex.

> > 2.   In a similar vein, I needed to make sure that an iteration
> > was supplied even when xrange or sequence types were supplied as
> arguments:
> >        gens = [ type(s)==types.GeneratorType and s or iter(s) for s in
> > sequences]
> > Was better replaced by:
> >        gens = [ hasattr(s,'next') and s or iter(s) for s in sequences]
> Functions that are idempotent when applied to already-OK arguments
> are preferable -- just as I wouldn't code:
>         x = isinstance(x, tuple) and x or tuple(x)
> even apart from issues of and/or, and typechecking, but just
>         x = tuple(x)
> knowing that this does return x if x is already a tuple.
> Similarly, in your case,
>         gens = map(iter, sequences)
> should work just fine!  We don't need to worry about whether s
> can be false in the and/or construct, etc, etc.

Hmmph!  I didn't know that.  My code was written to avoid unnecessarily
wrapping an iterator around an iterator.  I made this change immediately.

I haven't looked inside the source code for iterator, but would expect to
see something similar to the test hasattr(s,'next').  So, the point is still
valid.  The test is necessary; it is just hidden inside the function call.

> "Potentially-idempotent adapter functions are a honking great
> idea -- let's do more of those", to paraphrase the timbot.  PEP 246
> can be seen in that light, too: function adapt(object, protocol)
> returns object if object ALREADY satisfies the protocol.

Here, here!

> However, there IS an approach that is even better, and we have
> just finished talking about it: *potentially idempotent adapting
> functions*!  re.compile just happens to be one of those, thanks be:
> >>> a=re.compile(r'\d+')
> >>> b=re.compile(a)
> >>> a is b
> 1
> So just code
>     pattern = re.compile(pattern)
> and live happily ever after.

Humph! I didn't know that either.  Idempotence is cool!

Raymond Hettinger

More information about the Python-list mailing list