decorators tutorials

Jason tenax.raccoon at gmail.com
Mon Jul 23 19:25:21 CEST 2007


On Jul 23, 2:13 am, james_027 <cai.hai... at gmail.com> wrote:
> Hi,
>
> I am learning python by learning django, and I stumble upon decorator
> which is very cool, any beginners resources for python decorators,
> although I can google it, I just want to get a good tutorial for this
> topic.
>
> Thanks
> james

Remember that functions in Python are created when their containing
scope is parsed.  Let's suppose you want a way to help you debug
function calls.  This should catch any uncaught exceptions, log a
message to the logging system, and start a pdb interpreter.  It should
allow the SystemExit exception to continue to propogate without doing
those actions.  The following example would do that appropriately:

def HelpDebug(func, *args, **keyargs):
    "Assist with debugging a function."
    # The func argument is a Pythong function object
    # arg is the positional arguments that follow the
    # first parameter, and keyargs has the keyword arguments.
    try:
        returnValue = func(*args, **keyargs)
        return returnValue
    except SystemExit:
        raise  # Reraise the system exit exception
    except Exception, error:
        from logging import DEBUG, log
        from sys import exc_info
        import pdb

        log(DEBUG, "Caught Exception: %s", error)
        # Start the debugger at the place where the
        # exception occurred
        pdb.post_mortem(exc_info()[2])
    return  # Nothing to return when an exception occurred

def DivXY(x, y):
    "Divides X by Y"
    return x / y


# Debug the following calls
HelpDebug(DivXY, 5.0, 2.1)  # This will succeed
HelpDebug(DivXY, 10.0, 0.0) # Causes a ZeroDivisionError exception

There's a serious problem, though.  If you'd like to remove the
debugging effect, you need to change all your "HelpDebug(DivXY,
xvalue, yvalue)" calls to "DivXY(xvalue, yvalue)".  Besides, you're
trying to get the result of DivXY, not HelpDebug.  The calling code
shouldn't care about HelpDebug at all.  We could do change HelpDebug
so it always calls DivXY, but then we need to create a new function
for each one we want to debug.

This is where nested scopes come in.  Other code should care about
calling DivXY, but not HelpDebug.  Furthermore, the caller shouldn't
be able to casually tell the difference between calling a regular
DivXY or the HelpDebug'ed DivXY.

Well, if the caller shouldn't be able to casually tell the difference
between DivXY with or without the debugging code, then we need to be
able to do the following:
value1 = DivXY(5.0, 2.1)           # The regular call
value2 = HelpDebugDivXY(5.0, 2.1)  # The debugged call

We could create a special "HelpDebugDivXY" function for every such
function, but that would quickly exhaust our patience, and introduce
lots of extra code that can have bugs.  Python has nested scoping,
though.  If you nest a function inside another function, the inner
function has access to the names in the outer function.

That doesn't sound too spectacular at first.  HOWEVER, in Python
functions are objects!  The outer function can return the inner
function object.  Even though the outer function has returned, /the
inner function still has access to the outer function's names/!  For
example:
>>> def GetPrintXFunc(x):
...     "Returns a function that takes no parameters, but prints the
argument"
...     # Inner function starts just below
...     def PrintX():
...        "Prints the object bound to the name x in enclosing scope."
...        print "X is", x
...     # A function object is now bound to the name "PrintX"
...     return PrintX
...
>>> PrintSpam = GetPrintXFunc("Spam")
>>> PrintPi = GetPrintXFunc(3.14159)
>>> PrintSpam()
X is Spam
>>> PrintPi()
X is 3.14159
>>>

Both PrintSpam and PrintPi keep access to their enclosing scope's
arguments!  In Python, the "def" statement is actually a statement...
and IT ISN'T EVALUATED UNTIL ITS ENCLOSING SCOPE IS EXECUTED.

Take that in.  That is entirely unlike C or C++, where a function
definition is evaluated at compile time.  In Python, all statements
are evaluated at run time.  This works even for nested classes.
Here's the modified code to help us debug:

def WrapWithHelpDebug(func):
    """Returns a function object that transparently wraps the
parameter
    with the HelpDebug function"""

    # The def statement actually defines a function object and binds
    # it to a name.  Since it is nested in this function, the def
    # statement isn't evaluated until
    def HelpDebug(*args, **keyargs):
        "Assist with debugging a function."
        # The func argument is a Pythong function object
        # arg is the positional arguments that follow the
        # first parameter, and keyargs has the keyword arguments.
        try:
            # The name "func" comes from the outer scope,
WrapWithHelpDebug
            returnValue = func(*args, **keyargs)
            return returnValue
        except SystemExit:
            raise  # Reraise the system exit exception
        except Exception, error:
            from logging import DEBUG, log
            from sys import exc_info
            import pdb

            log(DEBUG, "Caught Exception: %s", error)
            # Start the debugger at the place where the
            # exception occurred
            pdb.post_mortem(exc_info()[2])
        return  # Nothing to return when an exception occurred

    # Back in the WrapWithHelpDebug scope.
    # HelpDebug is now a function objected defined in this scope.

def DivXY(x, y):
    "Divides X by Y"
    return x / y

DebugDivXY = WrapWithHelpDebug(DivXY)

# Debug the following calls
DivXY(5.0, 2.1)  # This will succeed
DebugDivXY(10.0, 0.0) # Causes a ZeroDivisionError exception

Wow, that's pretty easy, isn't it!  But, it's not as easy as it could
be.  Let's say that DivXY is used a lot, and you want to debug all
calls to it.  Rather than change the calls to "DivXY" to "DebugDivXY"
everywhere, we can simply assign the wrapped function object to the
DivXY name.
Instead of:
  DebugDivXY = WrapWithHelpDebug(DivXY)
We use:
  DivXY = WrapWithHelpDebug(DivXY)

Now, all calls to DivXY call the HelpDebug object that uses our
original DivXY function!  This is *exactly* what a decorator does.
Instead of writing:

def DivXY(x, y):
    "Divides X by Y"
    return x / y
DivXY = WrapWithHelpDebug(DivXY)

You write:

@WrapWithHelpDebug
def DivXY(x, y):
    "Divides X by Y"
    return x / y

To turn off the help-debug behavior, simply comment out the decorator.

This has turned out to be a bit long, so I'll finish up in the next
post....

  --Jason




More information about the Python-list mailing list