Re: [Python-ideas] Default arguments in Python - the return - running out of ideas but...

Greg Falcon a écrit :
Aowh... I didn't even think this was legal, but indeed this works... so it's obviously dead for "yield".
Well, not only do we have to write more code, but we lose in self-documentation I guess (I'd rather have the default appearing in the signature than in the function code - pydoc and stuffs won't notice it), and I find slightly disappointing the principle of a "sentinel", i.e "I would have wanted to do something there but I can't so I'll do it farther, take that in the meantime".
I must admit I'm totally out of ideas then. Regards, Pascal

Here's what I'd like: def myfunc(a, b, c = *lambda: expression): stuff The use of the lambda keyword here makes the scope of any variables in the expression clear. The use of the prefix * makes the syntax invalid today, suggests dereferencing and doesn't hide the overhead. This is equivalent to: __unset = object() __default = lambda: expression def mfunc(a, b, c = __unset): if c == __unset: c = __default() stuff --- Bruce

Bruce Leban wrote:
There is a proposal, which I thought was accepted in principle, to make '* seq' valid generally, not just in arg-lists. to mean 'unpack the sequence'. * (lambda:1,2)() would then be valid, and without the call, would be a runtime, not syntax error. Other than that ;0(, it would be an interesting idea.
tjr

On Wed, May 13, 2009 at 7:08 PM, Terry Reedy <tjreedy@udel.edu> wrote:
Then how about putting the * before the parameter ? def myfunc(a, b, *c = lambda: expression): It's currently a syntax error, although the fact that "*arg" and "*arg=default" would mean something completely different is problematic. Still the same idea can be applied for some other operator (either valid already or not). Regardless of the actual operator, I came up with the following additional subproposals. Subproposal (1): Get rid of the explicit lambda for dynamic arguments. That is, def myfunc(a, b, *x=[]): would be equivalent to what previous proposals would write as def myfunc(a, b, *x=lambda: []): Subproposal (2): If subproposal (1) is accepted, we could get for free (in terms of syntax at least) dynamic args depending on previous ones. That is, def myfunc(a, b, *m=(a+b)/2): would mean def myfunc(a, b, *m = lambda a,b: (a+b)/2): with the lambda being passed the values of a and b at runtime. Thoughts ? George

On Wed, May 13, 2009 at 5:10 PM, George Sakkis <george.sakkis@gmail.com>wrote:
I think similar syntax should do similar things. If *arg means one thing and &arg means something else, that's confusing. There are lots of non confusing alternatives: def foo(a, b := lambda: bar): def foo(a, b = & lambda: bar): def foo(a, @dynamic b = lambda: bar): # adding decorators on parameters and more Subproposal (1): Get rid of the explicit lambda for dynamic arguments. That
Explicit is better than implicit. There's a thunk getting created here, right? Don't you want that to be obvious? I do. Subproposal (2): If subproposal (1) is accepted, we could get for free
It's not free and that adds quite a bit of complexity. Note that default parameters are evaluated in the context of where the function is defined, NOT in the middle of setting the other function parameters. (That's true for this new case too.) Sure Lisp has let and let* but the proposal here is NOT to provide arbitrary computational ability in the parameter list but to provide a way to have defaults that are not static. We shouldn't over-engineer things just because we can. --- Bruce

On Wed, May 13, 2009 at 10:41 PM, Bruce Leban <bruce@leapyear.org> wrote:
Confusing?? It's certainly no more confusing than using *args for one thing and **args for something else.
Explicitness is in the eye of the beholder, and also an acquired taste. @decorator is less explicit than f = decorator(f) and yet it's generally considered a successful addition these days, despite the strong opposition it met initially.
Sure, but I'm not sure what's your point here. The compiler can generate bytecode to the effect of: def myfunc(a, b, *m = (a+b)/2): if m is not passed: m = default_m(a,b) # actual body follows Ideally an optimizer would further check whether it's safe to inline the expression (which should be the typical case) to avoid the function call overhead.
Agreed, the primary target is to fix the common gotcha of mutable defaults, and I'd rather see this handled than nothing at all. A secondary goal that can be achieved here though is to reduce the overusage of None (or other sentinels for that matter). There are several articles to the effect of "Null considered harmful"; also SQL as well as some statically typed languages provide both nullable and non-nullable types and assume the latter by default. The only useful operation to a sentinel is identity check, so you know that every `x = sentinel` assignment should be followed by one or more `if x is sentinel` checks. Fewer nulls means potentially less conditional logic mental overhead. I'm not claiming we should get rid of None of course; there are legitimate reasons for different behavior under different conditions. Here however we're talking about a very specific pattern: def f(a, b=sentinel): if b is sentinel: b = <expression> It may not seem such a big deal, but then again I don't think it's less trivial than the case for the (eventually) accepted ternary operator vs an if/else statement. George

On Thu, 14 May 2009 12:41:17 pm Bruce Leban wrote:
No, I don't want it to be obvious. I don't care about thunks, I care that x gets bound at runtime. I don't care what the implementation is: whether it is a thunk, eval(), voodoo or something else, just so long as it works. As for your argument that it is better to be explicit, when you want to add two numbers and compare them with a third, do you write: (1 .__add__(1)).__eq__(2) instead of 1+1 == 2? "Explicit is better than implicit", right? No. 1+1=2 *is* explicit, because that's the Python syntax for addition. All those double-underscore method calls are implementation details that do not belong in "standard" Python code. If Python develops new syntax for late-binding of default arguments, that too will be explicit, and any reference to thunks (or any other mechanism) will be an implementation detail. The syntax shouldn't depend on the implementation. lambda is already disliked by many people, including Guido. I don't think any suggestion that we make lambda more confusing by giving it two very different meanings ("create a thunk" inside function parameter lists, and "create a function" everywhere else) will be very popular on python-dev. -- Steven D'Aprano

On Thu, May 14, 2009 at 3:20 PM, Steven D'Aprano <steve@pearwood.info>wrote:
Absolutely not. This is a false analogy. The anology would be having an implicit multiplication operator and writing (a b c) instead of (a * b * c). <snip> The syntax shouldn't depend on the that it has right now. The new meaning is the decorator attached to the default assignment that says evaluate that lambda. I'll use an @dynamic decorator-like syntax to illustrate. These would be valid: def foo(a, b = @dynamic lambda: []): def foo(a, b = @dynamic lambda: list()): def foo(a, b = @dynamic list): def foo(a, b = @dynamic random.random): and this would not: def foo(a, b = @dynamic []) def foo(a, b = @dynamic 5) because @dynamic says that the thing that follows is called to generate a dynamic default parameter value and you can't call [] or 5. My point about creating a thunk is *not* an implementation detail. The point here is that if you use one of the forms above with a lambda, it's the lambda creating a thunk/closure/voodoo thing at this point in the program, *not* the @dynamic decorator. The scope of that lambda is exactly what it looks like it is with or without the @dynamic decorator. Likewise, in the random.random, example, it's the value of random.random at the time the function is defined, not some later value that might be assigned to that name. If you use some other syntax that doesn't look like a lambda, I have to learn the scoping rules for that syntax. I already know the rules for lambda. --- Bruce

Bruce Leban wrote:
Hmm, very interesting, but in your example what is "dynamic" doing? Are you proposing it as a keyword to signal "here comes a dynamic default"? Do we really need it? Why not something like this: def five_appender(x=@list): x.append(5) return x
The idea is that @ is a magic sigil meaning, "call this if no argument is passed in." So, as per your prior example @[] or @5 would be result in a runtime error, since they're not callable. If for some reason you want a fresh 5 (I can't think of why, since it's immutable, but whatever), you would need to use a lambda: def n_appender(n=@lambda: 5, x=@list): x.append(n) return x Do y'all think this is enough inline with how @ is already used to make sense? Or is it too different from the existing use of @? -- Carl Johnson

On May 15, 3:30 am, Carl Johnson <cmjohnson.mailingl...@gmail.com> wrote:
If we're making magical objects, why not just make a magical object that gives you the ability to defer the execution of a block of code until an operation is performed on it? That way at least it makes sense if you've learned the rest of the language. Geremy Condra

Le Wed, 13 May 2009 20:10:42 -0400, George Sakkis <george.sakkis@gmail.com> s'exprima ainsi:
While I understand the intent, this seems complicated to me. I find clearer to express m in the func body; using a sentinel if m is a real default arg (meaning it could possibly be passed by the user). UNDEF = object() def myfunc(a, b, m=UNDEF): if m is UNDEF: m = (a+b)/2) Generally speaking, I find ok the need of sentinels for clarifying rare and non-obvious cases such as runtime-changing default values: def somefunc(arg, m=UNDEF): if m is UNDEF: m = runtimeDefaultVal() While I do not find ok the need of a sentinel to avoid the common gotcha of a default value beeing "back-updated" when the corresponding local var is changed in the func body: def otherfunc(arg, l=UNDEF): if l is UNDEF: l = [] <possibly update l> Denis ------ la vita e estrany

On Thu, 14 May 2009 09:44:07 pm spir wrote:
But those two idioms are the same thing! In the first case, if m is not provided by the caller, your function has to produce a fresh object at runtime. It does this by calling runtimeDefaultVal() which returns some unspecified object. In the second case, if l is not provided by the caller, your function has to produce a fresh object at runtime. It does this by calling []. This is merely a special case of the first case, where runtimeDefaultVal() simply returns [] every time. -- Steven D'Aprano

Here's what I'd like: def myfunc(a, b, c = *lambda: expression): stuff The use of the lambda keyword here makes the scope of any variables in the expression clear. The use of the prefix * makes the syntax invalid today, suggests dereferencing and doesn't hide the overhead. This is equivalent to: __unset = object() __default = lambda: expression def mfunc(a, b, c = __unset): if c == __unset: c = __default() stuff --- Bruce

Bruce Leban wrote:
There is a proposal, which I thought was accepted in principle, to make '* seq' valid generally, not just in arg-lists. to mean 'unpack the sequence'. * (lambda:1,2)() would then be valid, and without the call, would be a runtime, not syntax error. Other than that ;0(, it would be an interesting idea.
tjr

On Wed, May 13, 2009 at 7:08 PM, Terry Reedy <tjreedy@udel.edu> wrote:
Then how about putting the * before the parameter ? def myfunc(a, b, *c = lambda: expression): It's currently a syntax error, although the fact that "*arg" and "*arg=default" would mean something completely different is problematic. Still the same idea can be applied for some other operator (either valid already or not). Regardless of the actual operator, I came up with the following additional subproposals. Subproposal (1): Get rid of the explicit lambda for dynamic arguments. That is, def myfunc(a, b, *x=[]): would be equivalent to what previous proposals would write as def myfunc(a, b, *x=lambda: []): Subproposal (2): If subproposal (1) is accepted, we could get for free (in terms of syntax at least) dynamic args depending on previous ones. That is, def myfunc(a, b, *m=(a+b)/2): would mean def myfunc(a, b, *m = lambda a,b: (a+b)/2): with the lambda being passed the values of a and b at runtime. Thoughts ? George

On Wed, May 13, 2009 at 5:10 PM, George Sakkis <george.sakkis@gmail.com>wrote:
I think similar syntax should do similar things. If *arg means one thing and &arg means something else, that's confusing. There are lots of non confusing alternatives: def foo(a, b := lambda: bar): def foo(a, b = & lambda: bar): def foo(a, @dynamic b = lambda: bar): # adding decorators on parameters and more Subproposal (1): Get rid of the explicit lambda for dynamic arguments. That
Explicit is better than implicit. There's a thunk getting created here, right? Don't you want that to be obvious? I do. Subproposal (2): If subproposal (1) is accepted, we could get for free
It's not free and that adds quite a bit of complexity. Note that default parameters are evaluated in the context of where the function is defined, NOT in the middle of setting the other function parameters. (That's true for this new case too.) Sure Lisp has let and let* but the proposal here is NOT to provide arbitrary computational ability in the parameter list but to provide a way to have defaults that are not static. We shouldn't over-engineer things just because we can. --- Bruce

On Wed, May 13, 2009 at 10:41 PM, Bruce Leban <bruce@leapyear.org> wrote:
Confusing?? It's certainly no more confusing than using *args for one thing and **args for something else.
Explicitness is in the eye of the beholder, and also an acquired taste. @decorator is less explicit than f = decorator(f) and yet it's generally considered a successful addition these days, despite the strong opposition it met initially.
Sure, but I'm not sure what's your point here. The compiler can generate bytecode to the effect of: def myfunc(a, b, *m = (a+b)/2): if m is not passed: m = default_m(a,b) # actual body follows Ideally an optimizer would further check whether it's safe to inline the expression (which should be the typical case) to avoid the function call overhead.
Agreed, the primary target is to fix the common gotcha of mutable defaults, and I'd rather see this handled than nothing at all. A secondary goal that can be achieved here though is to reduce the overusage of None (or other sentinels for that matter). There are several articles to the effect of "Null considered harmful"; also SQL as well as some statically typed languages provide both nullable and non-nullable types and assume the latter by default. The only useful operation to a sentinel is identity check, so you know that every `x = sentinel` assignment should be followed by one or more `if x is sentinel` checks. Fewer nulls means potentially less conditional logic mental overhead. I'm not claiming we should get rid of None of course; there are legitimate reasons for different behavior under different conditions. Here however we're talking about a very specific pattern: def f(a, b=sentinel): if b is sentinel: b = <expression> It may not seem such a big deal, but then again I don't think it's less trivial than the case for the (eventually) accepted ternary operator vs an if/else statement. George

On Thu, 14 May 2009 12:41:17 pm Bruce Leban wrote:
No, I don't want it to be obvious. I don't care about thunks, I care that x gets bound at runtime. I don't care what the implementation is: whether it is a thunk, eval(), voodoo or something else, just so long as it works. As for your argument that it is better to be explicit, when you want to add two numbers and compare them with a third, do you write: (1 .__add__(1)).__eq__(2) instead of 1+1 == 2? "Explicit is better than implicit", right? No. 1+1=2 *is* explicit, because that's the Python syntax for addition. All those double-underscore method calls are implementation details that do not belong in "standard" Python code. If Python develops new syntax for late-binding of default arguments, that too will be explicit, and any reference to thunks (or any other mechanism) will be an implementation detail. The syntax shouldn't depend on the implementation. lambda is already disliked by many people, including Guido. I don't think any suggestion that we make lambda more confusing by giving it two very different meanings ("create a thunk" inside function parameter lists, and "create a function" everywhere else) will be very popular on python-dev. -- Steven D'Aprano

On Thu, May 14, 2009 at 3:20 PM, Steven D'Aprano <steve@pearwood.info>wrote:
Absolutely not. This is a false analogy. The anology would be having an implicit multiplication operator and writing (a b c) instead of (a * b * c). <snip> The syntax shouldn't depend on the that it has right now. The new meaning is the decorator attached to the default assignment that says evaluate that lambda. I'll use an @dynamic decorator-like syntax to illustrate. These would be valid: def foo(a, b = @dynamic lambda: []): def foo(a, b = @dynamic lambda: list()): def foo(a, b = @dynamic list): def foo(a, b = @dynamic random.random): and this would not: def foo(a, b = @dynamic []) def foo(a, b = @dynamic 5) because @dynamic says that the thing that follows is called to generate a dynamic default parameter value and you can't call [] or 5. My point about creating a thunk is *not* an implementation detail. The point here is that if you use one of the forms above with a lambda, it's the lambda creating a thunk/closure/voodoo thing at this point in the program, *not* the @dynamic decorator. The scope of that lambda is exactly what it looks like it is with or without the @dynamic decorator. Likewise, in the random.random, example, it's the value of random.random at the time the function is defined, not some later value that might be assigned to that name. If you use some other syntax that doesn't look like a lambda, I have to learn the scoping rules for that syntax. I already know the rules for lambda. --- Bruce

Bruce Leban wrote:
Hmm, very interesting, but in your example what is "dynamic" doing? Are you proposing it as a keyword to signal "here comes a dynamic default"? Do we really need it? Why not something like this: def five_appender(x=@list): x.append(5) return x
The idea is that @ is a magic sigil meaning, "call this if no argument is passed in." So, as per your prior example @[] or @5 would be result in a runtime error, since they're not callable. If for some reason you want a fresh 5 (I can't think of why, since it's immutable, but whatever), you would need to use a lambda: def n_appender(n=@lambda: 5, x=@list): x.append(n) return x Do y'all think this is enough inline with how @ is already used to make sense? Or is it too different from the existing use of @? -- Carl Johnson

On May 15, 3:30 am, Carl Johnson <cmjohnson.mailingl...@gmail.com> wrote:
If we're making magical objects, why not just make a magical object that gives you the ability to defer the execution of a block of code until an operation is performed on it? That way at least it makes sense if you've learned the rest of the language. Geremy Condra

Le Wed, 13 May 2009 20:10:42 -0400, George Sakkis <george.sakkis@gmail.com> s'exprima ainsi:
While I understand the intent, this seems complicated to me. I find clearer to express m in the func body; using a sentinel if m is a real default arg (meaning it could possibly be passed by the user). UNDEF = object() def myfunc(a, b, m=UNDEF): if m is UNDEF: m = (a+b)/2) Generally speaking, I find ok the need of sentinels for clarifying rare and non-obvious cases such as runtime-changing default values: def somefunc(arg, m=UNDEF): if m is UNDEF: m = runtimeDefaultVal() While I do not find ok the need of a sentinel to avoid the common gotcha of a default value beeing "back-updated" when the corresponding local var is changed in the func body: def otherfunc(arg, l=UNDEF): if l is UNDEF: l = [] <possibly update l> Denis ------ la vita e estrany

On Thu, 14 May 2009 09:44:07 pm spir wrote:
But those two idioms are the same thing! In the first case, if m is not provided by the caller, your function has to produce a fresh object at runtime. It does this by calling runtimeDefaultVal() which returns some unspecified object. In the second case, if l is not provided by the caller, your function has to produce a fresh object at runtime. It does this by calling []. This is merely a special case of the first case, where runtimeDefaultVal() simply returns [] every time. -- Steven D'Aprano
participants (9)
-
Bruce Leban
-
Carl Johnson
-
CTO
-
Georg Brandl
-
George Sakkis
-
Pascal Chambon
-
spir
-
Steven D'Aprano
-
Terry Reedy