Credit: Alex Martelli
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.
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))
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).
Get Python Cookbook now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.