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

Chris Angelico rosuav at gmail.com
Thu Nov 19 14:46:42 EST 2015


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



More information about the Python-list mailing list