5.11 Checking if an Object Has Necessary Attributes
Credit: Alex Martelli
5.11.1 Problem
You need to check if an object has certain necessary attributes,
before performing state-altering operations, but you want to avoid
type-testing because you know it reduces polymorphism.
5.11.2 Solution
In Python, you normally try whatever operations you need to perform.
For example, here's the simplest, no-checks code for
manipulations of a list:
def munge1(alist):
alist.append(23)
alist.extend(range(5))
alist.append(42)
alist[4] = alist[3]
alist.extend(range(2))
While this is usually adequate, there may be occasional problems. For
example, if the alist object has an
append method but not an extend
method, the munge1 function will partially alter
alist before an exception is raised. Such partial
alterations are generally not cleanly undoable, and, depending on
your application, they can be quite a bother.
To avoid partial alteration, you might
want to check the type. A naive Look Before You Leap (LBYL) approach
looks safer, but it has a serious defect: it loses polymorphism.
The worst approach of all is
checking for equality of types:
def munge2(alist):
if type(alist)==type([]):
munge1(alist)
else: raise TypeError, "expected list, got %s"%type(alist)
A better, but still unfavorable, approach (which at least works for
list subclasses in 2.2) is using
isinstance:
def munge3(alist):
if isinstance(alist, type[]):
munge1(alist)
else: raise TypeError, "expected list, got %s"%type(alist)
The proper solution is accurate LBYL, which is safer and fully
polymorphic:
def munge4(alist):
# Extract all bound methods you need (immediate exception
# if any needed method is missing)
append = alist.append
extend = alist.extend
# Check operations, such as indexing, to raise
# exceptions ASAP if signature compatibility is missing
try: a[0]=a[0]
except IndexError: pass # An empty alist is okay
# Operate -- no exceptions expected at this point
append(23)
extend(range(5))
append(42)
alist[4] = alist[3]
extend(range(2))
5.11.3 Discussion
Python functions are naturally polymorphic on their arguments, and
checking argument types loses polymorphism. However, we may still get
early checks and some extra safety without any substantial cost.
The Easier to Ask Forgiveness than Permission (EAFP) approach, in
which we try operations and handle any resulting exceptions, is the
normal Pythonic way of life and usually works great. Explicit
checking of types severely restricts Python's normal
signature-based polymorphism and should be avoided in most cases.
However, if we need to perform several operations on an object,
trying to do them all could result in some of them succeeding and
partially altering the object before an exception is raised.
For example, suppose that munge1, in the
recipe's code, is called with an actual argument
value for alist that has an
append method but lacks extend.
In this case, alist will be altered by the first
call to append, and the attempt to call
extend will raise an exception, leaving
alist's state partially altered
in a way that may be hard to recover from. Sometimes, a sequence of
operations should be atomic: either all of the alterations happen or
none of them do.
We can get closer to that by switching to LBYL, but in an accurate,
careful way. Typically, we extract all bound methods
we'll need, then noninvasively test the necessary
operations (such as indexing on both sides of the assignment
operator). We move on to actually changing the object state only if
all of this succeeds. From there, it's far less
likely (though not impossible) that exceptions will occur in
midstream, with state partially altered.
This extra complication is pretty modest, and the slowdown due to the
checks is typically more or less compensated by the extra speed of
using bound methods versus explicit attribute access (at least if the
operations include loops, which is often the case).
It's important to avoid overdoing the checks, and
assert can help with that. For example, you can
add assert callable(append) to munge4(
). In this case, the compiler will remove the
assert entirely when the program is run with
optimization (i.e., with flags -O or
-OO), while performing the checks when the program
is run for testing and debugging (i.e., without the optimization
flags).
5.11.4 See Also
assert and the meaning of the
-O and -OO command-line
arguments are defined in all Python reference texts; the
Library Reference section on sequence types.
|