Welcome, guest | Sign In | My Account | Store | Cart

This recipe extends the standard python semantics with respect to default function arguments by allowing "deferred" expressions, expressions that are evaluated on every call instead of just once at function definition time.

Python, 46 lines
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import inspect

class Deferred(object):
    def __init__(self, expr):
        self.expr = expr

def revaluatable(func):
    varnames,_,_,defaults = inspect.getargspec(func)
    num_varnames = len(varnames); num_defaults = len(defaults)
    def wrapper(*args, **kwds):
        if len(args) >= num_varnames: # defaults not used here
            return func(*args,**kwds)
        f_locals = dict(zip(varnames,args))
        # maximum number of used defaults
        max_defaults = min(num_defaults, num_varnames-len(args))
        for var,default in zip(varnames[-max_defaults:],defaults[-max_defaults:]):
            if var in kwds: 
                continue    # passed as keyword argument; don't use the default
            if not isinstance(default, Deferred): 
                f_locals[var] = default # non re-evaluatable default
            else:                       # reevaluate default expr. in f_locals
                f_locals[var] = eval(default.expr, func.func_globals, f_locals)
        f_locals.update(kwds)           # add any extra keyword arguments
        return func(**f_locals)
    return wrapper


#======= example ===============================================================    

>>> G = 1   # some global
>>>
>>> @revaluatable
... def f(w, x=Deferred('x**2+G'), y=Deferred('w+x'), z=Deferred('[]'))
...     z.extend([w,x,y]); return z
...
>>> f(3)
[3, 10, 13]
>>> G=3; f(4)
[4, 12, 16]
>>> f(4,5)
[4, 5, 9]
>>> f(-1,1,0)
[-1, 1, 0]
>>> from collections import deque
>>> f(-1, z=deque())
deque([-1, 12, 11])

A common pitfall for python beginners is to use a mutable default function argument (often an empty list or dict), expecting that a new instance is created on each call, while in fact function arguments are evaluated only once, when the function is defined. The standard python practice is to not use mutable default arguments in the first place, but instead use an immutable unique object (typically None) as a placeholder and create the mutable object inside the function: <pre> def foo(mapping=None): if mapping is None: mapping = {} </pre> Recipe 303440 [1] provides another approach: a decorator that keeps the default function arguments fresh by deep copying them on every call. This works for objects whose state can be reconstructed by deep copying, but some times this is not possible or meaningful. For instance, in a function such as <pre> def log(msg, stream=sys.stderr): print >> stream, msg </pre> deep copying the standard error file-like object doesn't make sense. Even if it did though, it wouldn't help if sys.stderr was rebinded to a different object after the function definition.

This recipe provides a more general approach that stores and reevaluates default argument expressions. This addresses not only mutable objects and simple attribute lookups such as sys.stderrr, but arbitrary expressions. The stored expressions are reevaluated in the functions globals and locals, in the same order the parameters are defined. This allows one parameter to default to an expression that involves other previously defined parameters, as illustrated in the example.

Why would one use this decorator ? I see two main advantages: - Documentation: deferred expressions are self documenting. On the other hand, using a placeholder such as None requires either explicit documentation or manually digging in the code to see what the actual value to be used in the default case is. - Eliminates the 'if var is None: var = [expression]' boilerplate logic and error-proneness (if None happens to be a legitimate value returned by the expression instead of a special flag).

[1] http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/303440

2 comments

Calvin Spealman 14 years, 9 months ago  # | flag

Good but no cigar. This could be useful, but I think the name could be better. Deferred is a very common name, used from the popular Twisted project, so you immediately put an extra burden for anyone wanting to use this with a Twisted project. Maybe you could call it RuntimeDefault or something like that?

Also, what about callables instead of strings? I'm always opposed to strings of code.

George Sakkis (author) 14 years, 9 months ago  # | flag

Indeed, selecting a meaningful yet short name is the toughest part. Deferred is a good compromise but you're right, it's already taken. As for callables vs strings, I also avoid the latter in general, but here they offer two advantages:

  1. Brevity: Ideally re-evalutable defaults shouldn't be harder to spell than once-only defaults. Having to write something like f(x=RutimeDefault(lambda: [])) would render it practically unusable. If they are common enough, even a special syntax could be justified.

  2. Makes accessible the previously defined arguments in the expression. AFAIK trying to modify directly the func_locals of a function has no effect (let alone callables that are not functions).

Created by George Sakkis on Sat, 3 Feb 2007 (PSF)
Python recipes (4591)
George Sakkis's recipes (26)

Required Modules

  • (none specified)

Other Information and Tasks