using varargs in function decorations

2 views
Skip to first unread message

Chad MILLER

unread,
May 19, 2009, 5:53:01 AM5/19/09
to orlan...@googlegroups.com
This will be an advanced topic, starting from our previous moderate topic about variable arguments.

In my last message to the list, I covered packing iterables into and unpacking tuples from the parameter lists of functions.  Remember this means that if you have something like a list,  l , with values you know should be used to call a function, you can use the special form  *l  to fill the parameter list.  An example:

>>> l = [13, 29, 42]
>>> def f(a, b, c):
        pass

You could call it as either of

>>> f(l[0], l[1], l[2])       # or
>>> f(*l)

Additionally, it's symmetrical on the parameter definition end.  An asterisk-name form at any position makes the name a tuple containing all the remaining values supplied in the call of the function.  (There can be only one.  Or zero.)

>>> def f(a, *b):
        print b
>>> f(13, 29, 42)  ->  (29, 42)
>>> f(13)          ->  ()

Named parameters are treated separately from positional parameters.  Since they can be in any order and are associated with a name, the natural data structure for it is the dictionary.  Instead of one asterisk, named params dictionaries are specified with two asterisks.

>>> def f(*args, **kwargs):
        pass
>>> d = { "tasty":"H20", "not so tasty":"H2SO4" }
>>> f(*l, **d)

The combination of positional parameters and named parameters in tuples and dictionaries can catch and dispatch any function call in Python. 

In this way, we can make functions that could proxy a call to another function, because we don't have to worry about the count of parameters.  We know how to catch any possible function call and that we can dispatch any function call with tuple or dict.  We can now think about wrapping arbitrary function calls, to make powerful idioms for programming.

----

New in Python 2.5 is decoration of functions, expressed as the at-symbol and another function name, before the definition of a function.  Decoration makes a wrapper around a function definition so that the wrapper is called instead, and that wrapper may then call the original function.  The wrapper may do anything to the argument list or the return value, or add other function calls before or after the call of the original, target function.

>>> @wrap
>>> def f(a, b, c):
        print b

The simplest case of decoration is this example below.  It might be a little confusing because we define three functions, nested, to get the job done.

We make a function that sets-up and instantiates a decorator.  This is called immediately when you use the decorator in the code.  If you give parameters to the at-decorator name, this is the function that receives them, this is the function that returns the callable object that is assigned in place of the decorated, defined function.  Think of this as the set-up function

Inside it is the actual decorator, which is the function that gets assigned to the real name of your target function.  This callable object is (almost always) what gets returned from the set-up function.  Its only parameter is the function that we are decorating.  This scope is what gives the upcoming proxy function the value of what it's going to call.

Inside the decorator is a function that proxies the call to your function.  It uses our trick of variable arguments to pass through all that you send to your function in the call of it.  It knows what to call because the scope of the parent, the "closure", received the callable object as a parameter.


def wrap():
    def function_decorator(callable):
        """This is the function python calls to perform the decoration."""

        def decorated_function(*args, **kwargs):
            """This is the decorated function."""
            return callable(*args, **kwargs)

        return decorated_function

    return function_decorator


Now let's look at decorator syntax,
>>> @wrap
>>> def foo():
        pass

This is effectively the same as

>>> def foo():
        pass
>>> foo = wrap()(foo)

Decoration can be chained together, with the first use being the outermost execution shell.

>>> @synchronize
>>> @log("info")
>>> @deprecated(in_version=4.7)
>>> def whargaaaarbl(f):
        pass

This permits one to make general functions that operate on the definition of functions, like enforcing mutexes around function calls, or logging function calls, or prefixing or suffixing function calls with other calls, or disabling a function altogether, or marking a function as deprecated after a particular source version.

Our new trick with parameters makes it possible to write generic decorators where we need not worry about the possible variety of parameters.  Decorators are the kind of programming tool that you need not create every day, but are necessary to know about because they solve some kinds of code problems elegantly.
Reply all
Reply to author
Forward
0 new messages