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

Chris Angelico rosuav at gmail.com
Mon Nov 23 12:05:13 EST 2015


On Tue, Nov 24, 2015 at 3:40 AM, Ian Kelly <ian.g.kelly at gmail.com> wrote:
> On Mon, Nov 23, 2015 at 1:23 AM, Chris Angelico <rosuav at gmail.com> wrote:
>> 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.
>
> You should be able to get the correct globals from f.__globals__.
>
> For locals, the decorator might capture the locals of the previous
> stack frame at the moment the decorator was called, but that's
> potentially a pretty heavy thing to be retaining for this purpose; the
> definition of a @latearg function would indefinitely keep a reference
> to every single object that was bound to a variable in that scope, not
> just the things it needs. For better specificity you could parse the
> expression and then just grab the names that it uses. Even so, this
> would still act somewhat like early binding in that it would reference
> the local variables at the time of definition rather than evaluation.
>
> Nonlocals? Just forget about it.

And since nonlocals are fundamentally unsolvable, I took the simpler
option and just used an actual lambda function.

>> 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"
>
> A parameter annotation could be used in conjunction with the decorator.
>
> @latearg
> def x(y: latearg = lambda: []):
>     ...
>
> But that's even more verbose. In the simple case where all the
> defaults should be late, one could have something like:
>
> @latearg('*')
> def x(y=lambda: []):
>     ...
>
> The argument could be generalized to pass a set of parameter names as
> an alternative to the annotation.

Yeah, that might help. With real compiler support, both of these could
be solved:

def x(y=>[]):
    ...

The displayed default could be "=>[]", exactly the way it's seen in
the source, and the run-time would know exactly which args were
flagged this way. Plus, it could potentially use a single closure to
evaluate all the arguments.

>> 3) Keyword args aren't handled - they're passed through as-is (and
>> keyword-only arg defaults aren't rendered)
>
> I would expect that Python 3 Signature objects would make this a lot
> simpler to handle.

Maybe. It's still going to be pretty complicated. I could easily
handle keyword-only arguments, but recognizing that something in **kw
is replacing something in *a is a bit harder. Expansion invited.

This is reminiscent of the manual "yield from" implementation in PEP
380. It looks simple enough, until you start writing in all the corner
cases.

ChrisA



More information about the Python-list mailing list