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

Todd toddrjen at gmail.com
Sat Nov 21 03:38:01 EST 2015


On Nov 19, 2015 20:48, "Chris Angelico" <rosuav at gmail.com> wrote:
>
> On Fri, Nov 20, 2015 at 5:42 AM, Ian Kelly <ian.g.kelly at gmail.com> wrote:
> > BartC on the other hand is just complaining about an aspect of Python
> > that is legitimately controversial.
>
> IMO it's controversial mainly because there's an easy and obvious
> syntax for early binding, but late binding doesn't have syntactic
> support, and all the options are imperfect. Consider:
>
> def late1(x=None):
>     """Terse but buggy"""
>     do_stuff_with(x or [])
>
> def late2(x=None):
>     """Not bad but has an extra line for each default. Also, can't take
None."""
>     if x is None: x = []
>     do_stuff_with(x)
>
> _SENTINEL = object()
> def late3(x=_SENTINEL):
>     """Has the same global-pollution problem you get when you
>     try to construct early binding from late; you can share the
>     sentinel among multiple args, even multiple funcs, though"""
>     if x is _SENTINEL: x = []
>     do_stuff_with(x)
>
> def late4(x=object()):
>     """Depends on its own name remaining bound in its scope,
>     and will break if you change argument order"""
>     if x is late4.__defaults__[0]: x = []
>     do_stuff_with(x)
>
> And there might be other techniques, too. They all share one important
> flaw, too: When you ask for help about the function, you can't see
> what the default really is. All you see is:
>
> late1(x=None)
> late3(x=<object object at 0x7f7a80b3a080>)
>
> In the first two cases, it's not too bad; you can specify a timeout as
> either a number or as None, and if it's None (the default), the
> timeout is three times the currently estimated round trip time. No
> problem. But how would you implement something like next() in pure
> Python? If you provide _any second argument at all_, it returns that
> instead of raising StopIteration. The ONLY way that I can think of is
> to use *args notation:
>
> def next(iterator, *default):
>     try:
>         return iterator.__next__()
>     except StopIteration:
>         if default: return default[0]
>         raise
>
> And while that isn't TOO bad for just one argument, it scales poorly:
>
> def foo(fixed_arg, *args):
>     """Foo the fixed_arg against the spam mapping, the
>     ham sequence, and the jam file."""
>     args.reverse()
>     spam = args.pop() if args else {}
>     ham = args.pop() if args else []
>     jam = args.pop() if args else open("jam.txt")
>
> Suppose, instead, that Python had a syntax like this:
>
> def foo(fixed_arg, spam=>{}, ham=>[], jam=>open("jam.txt")):
>     """Foo the fixed_arg against the spam mapping, the
>     ham sequence, and the jam file."""
>
> 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.
>
> Would this satisfy the people who get confused about "=[]"?
>
> ChrisA

Rather than a dedicated syntax, might this be something that could be
handled by a built-in decorator?

Maybe something like:

@late_binding
def myfunc(x, y=[]):

@late_binding('z')
def myfunc(x, y=expensive(), z=[]):

M = 5
@late_binding('z', vars_early=True)
def myfunc(x, y=expensive(), z=list(range(n))):

The big advantage, as shown in the last example, is that it could be
possible to specify early our late binding for variables as well.



More information about the Python-list mailing list