Late-binding of function defaults (was Re: What is a function parameter =[] for?)

Chris Angelico rosuav at gmail.com
Mon Nov 23 03:23:54 EST 2015


On Fri, Nov 20, 2015 at 6:46 AM, Chris Angelico <rosuav at gmail.com> wrote:
> The expressions would be evaluated as closures, using the same scope
> that the function's own definition used. (This won't keep things alive
> unnecessarily, as the function's body will be nested within that same
> scope anyway.) Bikeshed the syntax all you like, but this would be
> something to point people to: "here's how to get late-binding
> semantics". For the purposes of documentation, the exact text of the
> parameter definition could be retained, and like docstrings, they
> could be discarded in -OO mode.

Just out of interest, I had a shot at implementing this with a
decorator. Here's the code:

# -- cut --

import functools
import time

class lb:
    def __repr__(self): return "<late-bind>"

def latearg(f):
    tot_args = f.__code__.co_argcount
    min_args = tot_args - len(f.__defaults__)
    defs = f.__defaults__
    # With compiler help, we could get the original text as well as something
    # executable that works in the correct scope. Without compiler help, we
    # either use a lambda function, or an exec/eval monstrosity that can't use
    # the scope of its notional definition (since its *actual* definition will
    # be inside this decorator). Instead, just show a fixed bit of text.
    f.__defaults__ = tuple(lb() if callable(arg) else arg for arg in defs)
    @functools.wraps(f)
    def inner(*a,**kw):
        if len(a) < min_args: return f(*a, **kw) # Will trigger TypeError
        if len(a) < tot_args:
            more_args = defs[len(a)-tot_args:]
            a += tuple(arg() if callable(arg) else arg for arg in more_args)
        return f(*a,**kw)
    return inner

def sleeper(tm):
    """An expensive function."""
    t = time.monotonic()
    time.sleep(tm)
    return time.monotonic() - t - tm

seen_args = []
@latearg
def foo(spam, ham=lambda: [], val=lambda: sleeper(0.5)):
    print("%s: Ham %X with sleeper %s" % (spam, id(ham), val))
    seen_args.append(ham) # Keep all ham objects alive so IDs are unique

@latearg
def x(y=lambda: []):
    y.append(1)
    return y

print("Starting!")
foo("one-arg 1")
foo("one-arg 2")
foo("two-arg 1", [])
foo("two-arg 2", [])
foo("tri-arg 1", [], 0.0)
foo("tri-arg 2", [], 0.0)
print(x())
print(x())
print(x())
print(x([2]))
print(x([3]))
print(x([4]))
print("Done!")

# -- cut --


This does implement late binding, but:
1) The adornment is the rather verbose "lambda:", where I'd much
rather have something shorter
2) Since there's no way to recognize "the ones that were adorned", the
decorator checks for "anything callable"
3) Keyword args aren't handled - they're passed through as-is (and
keyword-only arg defaults aren't rendered)
4) As commented, the help text can't pick up the text of the function

But it does manage to render args at execution time, and the help()
for the function identifies the individual arguments correctly (thanks
to functools.wraps and the modified defaults - though this
implementation is a little unfriendly, mangling the original function
defaults instead of properly wrapping).

Clock this one up as "useless code that was fun to write".

ChrisA



More information about the Python-list mailing list