Python Cookbook 2Nd Edition Jun 1002005 [Electronic resources] نسخه متنی

اینجــــا یک کتابخانه دیجیتالی است

با بیش از 100000 منبع الکترونیکی رایگان به زبان فارسی ، عربی و انگلیسی

Python Cookbook 2Nd Edition Jun 1002005 [Electronic resources] - نسخه متنی

David Ascher, Alex Martelli, Anna Ravenscroft

| نمايش فراداده ، افزودن یک نقد و بررسی
افزودن به کتابخانه شخصی
ارسال به دوستان
جستجو در متن کتاب
بیشتر
تنظیمات قلم

فونت

اندازه قلم

+ - پیش فرض

حالت نمایش

روز نیمروز شب
جستجو در لغت نامه
بیشتر
لیست موضوعات
توضیحات
افزودن یادداشت جدید







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.


/ 394