Recipe 6.13. Checking Whether an Object Has Necessary AttributesCredit: Alex Martelli ProblemYou 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. SolutionIn 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)) DiscussionPython 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.
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 AlsoLanguage 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. |