Recipe6.13.Checking Whether an Object Has Necessary Attributes


Recipe 6.13. Checking Whether an Object Has Necessary Attributes

Credit: Alex Martelli

Problem

You need to check whether an object has certain necessary attributes before performing state-altering operations. However, you want to avoid type-testing because you know it interferes with polymorphism.

Solution

In Python, you normally just try performing whatever operations you need to perform. For example, here's the simplest, no-checks code for doing a certain sequence of manipulations on a list argument:

def munge1(alist):     alist.append(23)     alist.extend(range(5))     alist.append(42)     alist[4] = alist[3]     alist.extend(range(2))

If alist is missing any of the methods you're calling (explicitly, such as append and extend; or implicitly, such as the calls to _ _getitem_ _ and _ _setitem_ _ implied by the assignment statement alist[4] = alist[3]), the attempt to access and call a missing method raises an exception. Function munge1 makes no attempt to catch the exception, so the execution of munge1 terminates, and the exception propagates to the caller of munge1. The caller may choose to catch the exception and deal with it, or terminate execution and let the exception propagate further back along the chain of calls, as appropriate.

This approach is usually just fine, but problems may occasionally occur. Suppose, for example, that the alist object has an append method but not an extend method. In this peculiar case, the munge1 function partially alters alist before an exception is raised. Such partial alterations are generally not cleanly undoable; depending on your application, they can sometimes be a bother.

To forestall the "partial alterations" problem, the first approach that comes to mind is to check the type of alist. Such a naive "Look Before You Leap" (LBYL) approach may look safer than doing no checks at all, but LBYL has a serious defect: it loses polymorphism! The worst approach of all is checking for equality of types:

def munge2(alist):     if type(alist) is list:       # a very bad idea         munge1(alist)     else: raise TypeError, "expected list, got %s" % type(alist)

This even fails, without any good reason, when alist is an instance of a subclass of list. You can at least remove that huge defect by using isinstance instead:

def munge3(alist):     if isinstance(alist, list):         munge1(alist)     else: raise TypeError, "expected list, got %s" % type(alist)

However, munge3 still fails, needlessly, when alist is an instance of a type or class that mimics list but doesn't inherit from it. In other words, such type-checking sacrifices one of Python's great strengths: signature-based polymorphism. For example, you cannot pass to munge3 an instance of Python 2.4's collections.deque, which is a real pity because such a deque does supply all needed functionality and indeed can be passed to the original munge1 and work just fine. Probably a zillion sequence types are out there that, like deque, are quite acceptable to munge1 but not to munge3. Type-checking, even with isinstance, exacts an enormous price.

A far better solution is accurate LBYL, which is both safe and fully polymorphic:

def munge4(alist):     # Extract all bound methods you need (get immediate exception,     # without partial alteration, if any needed method is missing):     append = alist.append     extend = alist.extend     # Check operations, such as indexing, to get an exception ASAP     # if signature compatibility is missing:     try: alist[0] = alist[0]     except IndexError: pass    # An empty alist is okay     # Operate: no exceptions are expected from this point onwards     append(23)     extend(range(5))     append(42)     alist[4] = alist[3]     extend(range(2))

Discussion

Python functions are naturally polymorphic on their arguments because they essentially depend on the methods and behaviors of the arguments, not on the arguments' types. If you check the types of arguments, you sacrifice this precious polymorphism, so, don't! However, you may perform a few early checks to obtain some extra safety (particularly against partial alterations) without substantial costs.

What Is Polymorphism?

Polymorphism (from Greek roots meaning "many shapes") is the ability of code to deal with objects of different types in ways that are appropriate to each applicable type. Unfortunately, this useful term has been overloaded with all sorts of implications, to the point that many people think it's somehow connected with such concepts as overloading (specifying different functions depending on call-time signatures) or subtyping (i.e., subclassing), which it most definitely isn't.

Subclassing is often a useful implementation technique, but it's not a necessary condition for polymorphism. Overloading is right out: Python just doesn't let multiple objects with the same name live at the same time in the same scope, so you can't have several functions or methods with the same name and scope, distinguished only by their signaturesa minor annoyance, at worst: just rename those functions or methods so that their name suffices to distinguish them.

Python's functions are polymorphic (unless you take specific steps to break this very useful feature) because they just call methods on their arguments (explicitly or implicitly by performing operations such as arithmetic and indexing): as long as the arguments supply the needed methods, callable with the needed signatures, and those calls perform the appropriate behavior, everything just works.


The normal Pythonic way of life can be described as the Easier to Ask Forgiveness than Permission (EAFP) approach: just try to perform whatever operations you need, and either handle or propagate any exceptions that may result. It usually works great. The only real problem that occasionally arises is "partial alteration": when you need to perform several operations on an object, just trying to do them all in natural order could result in some of them succeeding, and partially altering the object, before an exception is raised.

For example, suppose that munge1, as shown at the start of this recipe's Solution, is called with an actual argument value for alist that has an append method but lacks extend. In this case, alist is altered by the first call to append; but then, the attempt to obtain and call extend raises an exception, leaving alist's state partially altered, a situation that may be hard to recover from. Sometimes, a sequence of operations should ideally be atomic: either all of the alterations happen, and everything is fine, or none of them do, and an exception gets raised.

You can get closer to ideal atomicity by switching to the LBYL approach, but in an accurate, careful way. Extract all bound methods you'll need, then noninvasively test the necessary operations (such as indexing on both sides of the assignment operator). Move on to actually changing the object state only if all of this succeeds. From that point onward, it's far less likely (although not impossible) that exceptions will occur in midstream, leaving state partially altered. You could not reach 100% safety even with the strictest type-checking, after all: for example, you might run out of memory just smack in the middle of your operations. So, with or without type-checking, you don't really ever guarantee atomicityyou just approach asymptotically to that desirable property.

Accurate LBYL generally offers a good trade-off in comparison to EAFP, assuming we need safeguards against partial alterations. The extra complication is modest, and the slowdown due to the checks is typically compensated by the extra speed gained by using bound methods through local names rather than explicit attribute access (at least if the operations include loops, which is often the case). It's important to avoid overdoing the checks, and the assert statement can help with that. For example, you can add such checks as assert callable(append) to munge4. In this case, the compiler removes the assert entirely when you run the program with optimization (i.e., with flags -O or -OO passed to the python command), while performing the checks when the program is run for testing and debugging (i.e., without the optimization flags).

See Also

Language Reference and Python in a Nutshell about assert and the meaning of the -O and -OO command-line arguments; Library Reference and Python in a Nutshell about sequence types, and lists in particular.



Python Cookbook
Python Cookbook
ISBN: 0596007973
EAN: 2147483647
Year: 2004
Pages: 420

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net