Checking if an Object Has Necessary Attributes

Credit: Alex Martelli

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.

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))

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).

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.

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.