assignment expressions: an alternative proposal
I propose to use the following syntax for assignment expressions:
( NAME = expr )
I know that it was proposed before and this idea was rejected, because
accidentally using '=' in place of '==' is a pain point in
C/C++/JavaScript.
That said, I believe we can still use this syntax as long as we impose
the following three restrictions on it:
1. Only NAME token is allowed as a single target.
2. Parenthesis are required.
3. Most importantly: it is *not* allowed to mask names in the current
local scope.
Let's see how each restriction affects the syntax in detail:
(1) NAME tokens only:
if (a[1] = value) # SyntaxError
if (a.attr = value) # SyntaxError
(2) Required parens disambiguate the new syntax from keyword-arguments
and prevent using '=' in place of '==':
if a = value # SyntaxError
if expr and a = value # SyntaxError
(3) No masking of existing names in local scope makes using '=' in
place of '==' by mistake even less probable:
flag = get_flag()
...
if (flag = 'win') # SyntaxError
# or
def foo(value):
if (value = 1) # SyntaxError
# or
py> (c = 1) and (c = 2) # SyntaxError
# etc
The following code snippets are perfectly valid though:
py> a = (b = (c = 3))
py> a, b, c
(3, 3, 3)
# and
py> f = lambda x: x * 10
py> [[(y = f(x)), x/y] for x in range(1,5)]
[[10, 0.1], [20, 0.1], [30, 0.1], [40, 0.1]]
# and
def read():
while (command = input("> ")) != "quit":
print('you entered', command)
# and
py> if (match = re.search(r'wor\w+', 'hello world')):
py. print(match)
On 24 April 2018 at 23:38, Yury Selivanov
I propose to use the following syntax for assignment expressions:
( NAME = expr )
I know that it was proposed before and this idea was rejected, because accidentally using '=' in place of '==' is a pain point in C/C++/JavaScript.
That said, I believe we can still use this syntax as long as we impose the following three restrictions on it:
1. Only NAME token is allowed as a single target.
2. Parenthesis are required.
3. Most importantly: it is *not* allowed to mask names in the current local scope.
While I agree this would be unambiguous to a computer, I think for most humans it would be experienced as a confusing set of arcane and arbitrary rules about what "=" means in Python. Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia
On Tue, Apr 24, 2018 at 9:46 AM, Nick Coghlan
On 24 April 2018 at 23:38, Yury Selivanov
wrote: I propose to use the following syntax for assignment expressions:
( NAME = expr )
I know that it was proposed before and this idea was rejected, because accidentally using '=' in place of '==' is a pain point in C/C++/JavaScript.
That said, I believe we can still use this syntax as long as we impose the following three restrictions on it:
1. Only NAME token is allowed as a single target.
2. Parenthesis are required.
3. Most importantly: it is *not* allowed to mask names in the current local scope.
While I agree this would be unambiguous to a computer, I think for most humans it would be experienced as a confusing set of arcane and arbitrary rules about what "=" means in Python.
I respectfully disagree. There are no "arcane and confusing rules" about "=", it's rather simple: "=" is always an assignment. "==" is always an equality check. Having two assignment operators feels way more arcane to me. Especially in Python guided by "there should be one way" Zen. Yury
On 24 April 2018 at 23:50, Yury Selivanov
On Tue, Apr 24, 2018 at 9:46 AM, Nick Coghlan
wrote: On 24 April 2018 at 23:38, Yury Selivanov
wrote: I propose to use the following syntax for assignment expressions:
( NAME = expr )
I know that it was proposed before and this idea was rejected, because accidentally using '=' in place of '==' is a pain point in C/C++/JavaScript.
That said, I believe we can still use this syntax as long as we impose the following three restrictions on it:
1. Only NAME token is allowed as a single target.
2. Parenthesis are required.
3. Most importantly: it is *not* allowed to mask names in the current local scope.
While I agree this would be unambiguous to a computer, I think for most humans it would be experienced as a confusing set of arcane and arbitrary rules about what "=" means in Python.
I respectfully disagree. There are no "arcane and confusing rules" about "=", it's rather simple:
"=" is always an assignment. "==" is always an equality check.
That's not the distinction I meant, I meant the difficulty of explaining the discrepancies in this list: a = 1 # Assignment (a = 1) # Also assignment a, b = 1, 2 # Tuple assignment (a, b = 1, 2) # SyntaxError. Why? a.b = 1 # Attribute assignment (a.b = 1) # SyntaxError. Why? a[b] = 1 # Subscript assignment (a[b] = 1) # SyntaxError. Why? (a=1), (b=2) # Two assignments a=1, b=2 # SyntaxError. Why? f(a=1, b=2) # Function call with keyword args (a=1, b=2) # SyntaxError. Why? if (a=1): pass # Assignment if a=1: pass # SyntaxError. Why? Whereas if binding expressions use a different symbol, the question is far less likely to arise, and if it does come up, then the answer is the same as the one for def statements vs lambda expressions: because one is a statement, and the other is an expression. Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia
On Tue, Apr 24, 2018 at 10:07 AM, Nick Coghlan
"=" is always an assignment. "==" is always an equality check.
That's not the distinction I meant, I meant the difficulty of explaining the discrepancies in this list:
a = 1 # Assignment (a = 1) # Also assignment
a, b = 1, 2 # Tuple assignment (a, b = 1, 2) # SyntaxError. Why?
... Whereas if binding expressions use a different symbol, the question is far less likely to arise, and if it does come up, then the answer is the same as the one for def statements vs lambda expressions: because one is a statement, and the other is an expression.
A lot of other questions arise though. PEP 572 proposes: a = 1 # assignment a := 1 # also assignment (a := 1) # also assignment (a = 1) # error, why? It's also difficult to explain which one to use when. The net result is that code will be littered with both at random places. That will decrease the readability of Python code at least for some users who have similar taste to myself. With '=' in expressions, the code will look uniform. There will be a simple rule to put parens around assignments in expression and use simple names. After one or two descriptive SyntaxError users will learn how this syntax works (like people learn everything in coding). This all is very subjective. Yury
On 25 April 2018 at 00:23, Yury Selivanov
A lot of other questions arise though. PEP 572 proposes:
a = 1 # assignment a := 1 # also assignment (a := 1) # also assignment (a = 1) # error, why?
That's just the typical assignment/expression dichotomy, though, which is genuinely confusing for learners (since expression-level Python and statement-level Python allow different constructs), but also not a new problem. All the other keywords that have both statement level and expression level use cases are structured as prefix operators in statement form, and some kind of infix operator in expression form, whereas this would be the first case where we offered a construct that used infix syntax for both its statement form and its expression form.
It's also difficult to explain which one to use when. The net result is that code will be littered with both at random places. That will decrease the readability of Python code at least for some users who have similar taste to myself.
That's a legitimate concern with PEP 572 (and part of why I'm somewhere between -1 and -0 on the ":=" spelling, although I'd be +0 on an "is=" spelling that riffs off the "is" comparison operator - using the "name is= expr" spelling in place of a regular assignment looks sufficiently odd that I'd expect the temptation to write it in place of "name = expr" when the latter is permitted would be low)
With '=' in expressions, the code will look uniform. There will be a simple rule to put parens around assignments in expression and use simple names. After one or two descriptive SyntaxError users will learn how this syntax works (like people learn everything in coding).
Except that they'll also find other discrepancies like: a = 1 a = 2 being OK, while: a = 1 (a = 2) fails with SyntaxError on the second line. Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia
On Wed, Apr 25, 2018 at 12:23 AM, Yury Selivanov
On Tue, Apr 24, 2018 at 10:07 AM, Nick Coghlan
wrote: "=" is always an assignment. "==" is always an equality check.
That's not the distinction I meant, I meant the difficulty of explaining the discrepancies in this list:
a = 1 # Assignment (a = 1) # Also assignment
a, b = 1, 2 # Tuple assignment (a, b = 1, 2) # SyntaxError. Why?
... Whereas if binding expressions use a different symbol, the question is far less likely to arise, and if it does come up, then the answer is the same as the one for def statements vs lambda expressions: because one is a statement, and the other is an expression.
A lot of other questions arise though. PEP 572 proposes:
a = 1 # assignment a := 1 # also assignment (a := 1) # also assignment (a = 1) # error, why?
Your third example is just the same as the second, with parentheses around it. In most of Python, parentheses (if legal) have no effect other than grouping; "a + b * c" is the same thing as "(a + b) * c", just done in the other order. The last one is a clear demonstration that "=" is a statement, not an expression. Are people confused by this sort of thing: if x > 1: print("x is more than 1") (if x > 1:) print("SyntaxError") ? Yes, the word 'if' does have meaning in an expression context, and yes, it has a similar meaning to the 'if' statement, but people don't parenthesize entire statements. You try that with assignment, you get an error, and bam, it's obvious that you ran into this particular case. ChrisA
On Tue, Apr 24, 2018 at 10:56 AM, Chris Angelico
A lot of other questions arise though. PEP 572 proposes:
a = 1 # assignment a := 1 # also assignment (a := 1) # also assignment (a = 1) # error, why?
Your third example is just the same as the second, with parentheses around it. In most of Python, parentheses (if legal) have no effect other than grouping; "a + b * c" is the same thing as "(a + b) * c", just done in the other order. The last one is a clear demonstration that "=" is a statement, not an expression. Are people confused by this sort of thing:
if x > 1: print("x is more than 1") (if x > 1:) print("SyntaxError")
This is a very far-fetched example :) My point was that when you see lots of '=' and ':=' used at the statement level, one might try to write "if x = 1" instead of "if x := 1" -- boom, we have an unexpected SyntaxError for some users. In my opinion adding *any* assignment expression syntax to Python *will* create this sort of issues. PEP 572 isn't free of them, my proposal isn't free of them. My proposal doesn't add a new ':=' operator at the cost of slightly complicating rules around '='. PEP 572 avoids complicating '=', but adds an entirely new form of assignment. Yury
On Wed, Apr 25, 2018 at 1:03 AM, Yury Selivanov
On Tue, Apr 24, 2018 at 10:56 AM, Chris Angelico
wrote: [..] A lot of other questions arise though. PEP 572 proposes:
a = 1 # assignment a := 1 # also assignment (a := 1) # also assignment (a = 1) # error, why?
Your third example is just the same as the second, with parentheses around it. In most of Python, parentheses (if legal) have no effect other than grouping; "a + b * c" is the same thing as "(a + b) * c", just done in the other order. The last one is a clear demonstration that "=" is a statement, not an expression. Are people confused by this sort of thing:
if x > 1: print("x is more than 1") (if x > 1:) print("SyntaxError")
This is a very far-fetched example :)
Heh, yes it is. But my point is that the parens are not creating a weird situation here. They're just showcasing a distinction: one of these is a statement, the other an expression. Which is the entire point of the different operator - one is a syntactic feature of a statement that creates one or more name bindings, the other is a binary operator which results in a name binding as well as a value. ChrisA
On Tue, Apr 24, 2018 at 11:03:35AM -0400, Yury Selivanov wrote:
My point was that when you see lots of '=' and ':=' used at the statement level, one might try to write "if x = 1" instead of "if x := 1" -- boom, we have an unexpected SyntaxError for some users.
That's a *good* thing. They will then learn not to write x = 1 as an expression. Also, if I write lots of x := 1 binding-expressions as statements, my code is bad and deserves to fail code-review. But why would I write the extra colon (one character, two key-presses) to use x := 1 as a statement, when x = 1 will work? That's a sure sign that I don't know what I'm doing. (Or that I desperately wish I was writing Pascal.) I don't think we need worry about this. The sort of code that is filled with binding-expressions used as statements will almost certainly be so un-Pythonic and ugly in many other ways, that this won't make any difference.
In my opinion adding *any* assignment expression syntax to Python *will* create this sort of issues. PEP 572 isn't free of them, my proposal isn't free of them. My proposal doesn't add a new ':=' operator at the cost of slightly complicating rules around '='. PEP 572 avoids complicating '=', but adds an entirely new form of assignment.
Indeed. That is true: either way, we introduce complexity into the language. (But that will allow us to reduce complexity in *our* code.) Given that increasing complexity is inevitable regardless of whether we choose PEP 572 or your suggestion, it is better to choose the option which keeps binding-expressions and assignment statements separate, since they are two different concepts. -- Steve
On Tue, Apr 24, 2018 at 11:27 AM, Steven D'Aprano
On Tue, Apr 24, 2018 at 11:03:35AM -0400, Yury Selivanov wrote:
My point was that when you see lots of '=' and ':=' used at the statement level, one might try to write "if x = 1" instead of "if x := 1" -- boom, we have an unexpected SyntaxError for some users.
That's a *good* thing. They will then learn not to write x = 1 as an expression.
Also, if I write lots of x := 1 binding-expressions as statements, my code is bad and deserves to fail code-review. But why would I write the extra colon (one character, two key-presses) to use
x := 1
as a statement, when x = 1 will work? That's a sure sign that I don't know what I'm doing. (Or that I desperately wish I was writing Pascal.)
In JavaScript there's a new backticks syntax for string—their variant of f-strings. I'm seeing a lot of JS coders that use backticks everywhere, regardless if there's formatting in them or not. The result is that some JS code in popular libraries has now *three* different string literal syntaxes separated by one line of code. It looks weird. I expect to see something similar in Python code if we adapt ':='. I don't think the language will benefit from this. FWIW I'm fine with keeping the status quo and not adding new syntax at all. Yury
On 24/04/18 14:50, Yury Selivanov wrote:
On Tue, Apr 24, 2018 at 9:46 AM, Nick Coghlan
wrote: On 24 April 2018 at 23:38, Yury Selivanov
wrote: I propose to use the following syntax for assignment expressions:
( NAME = expr )
I know that it was proposed before and this idea was rejected, because accidentally using '=' in place of '==' is a pain point in C/C++/JavaScript.
That said, I believe we can still use this syntax as long as we impose the following three restrictions on it:
1. Only NAME token is allowed as a single target.
2. Parenthesis are required.
3. Most importantly: it is *not* allowed to mask names in the current local scope. While I agree this would be unambiguous to a computer, I think for most humans it would be experienced as a confusing set of arcane and arbitrary rules about what "=" means in Python. I respectfully disagree. There are no "arcane and confusing rules" about "=", it's rather simple:
"=" is always an assignment. But it isn't - in your proposed syntax :
* *<target> = <value>* is an assignment with no return value * *(<target> = <value>)* is an assignment with a returned value So now '=' is always an assignment, it is an assignment with extra semantics depending on surrounding syntax. As discussed previously by others on this exact proposals, you now have the issue of confusion when using keyword arguments : *my_func(a = b)* : clearly that is a call to `my_func' where argument a has the value of b, but if you want to do an assigment expression when calling the function you now have to do *my_func((a=b)) -* which frankly looks messy in my opinion; you get the same issue when you are wanting to do assignment expressions in tuples. Using a different operator for assignments which return values avoids the messy potentially multiple level brackets, and means that the semantics of an operator depends only on that operator and not on syntax elements before and after it. -- -- Anthony Flury email : *Anthony.flury@btinternet.com* Twitter : *@TonyFlury https://twitter.com/TonyFlury/*
On Tue, Apr 24, 2018 at 10:54 AM, Anthony Flury via Python-Dev
As discussed previously by others on this exact proposals, you now have the issue of confusion when using keyword arguments : *my_func(a = b)* : clearly that is a call to `my_func' where argument a has the value of b, but if you want to do an assigment expression when calling the function you now have to do *my_func((a=b)) -* which frankly looks messy in my opinion; you get the same issue when you are wanting to do assignment expressions in tuples.
Well, `my_func(a=(b:=foo))` or `my_func(b:=foo)` are also barely readable to my eye. My expectation is that users won't use any form of assignment expressions in function calls, it's painful with both proposals. Yury
On Tue, Apr 24, 2018 at 11:05:57AM -0400, Yury Selivanov wrote:
Well, `my_func(a=(b:=foo))` or `my_func(b:=foo)` are also barely readable to my eye.
There's no advantage to using binding-expressions unless you're going to re-use the name you just defined, and that re-use will give you a hint as to what is happening: my_func(arg, buffer=(buf := [None]*get_size()), size=len(buf))
My expectation is that users won't use any form of assignment expressions in function calls, it's painful with both proposals.
If binding-expressions are accepted into the language, I will certainly use them in function calls, *if and when appropriate*. I don't expect it will be common, but I'm sure it will happen. -- Steve
On Tue, Apr 24, 2018 at 11:34 AM, Steven D'Aprano
On Tue, Apr 24, 2018 at 11:05:57AM -0400, Yury Selivanov wrote:
Well, `my_func(a=(b:=foo))` or `my_func(b:=foo)` are also barely readable to my eye.
There's no advantage to using binding-expressions unless you're going to re-use the name you just defined, and that re-use will give you a hint as to what is happening:
my_func(arg, buffer=(buf := [None]*get_size()), size=len(buf))
Again, this is very subjective, but this code would fail my code review :) Don't you find buf = [None] * get_size() my_func(arg, buffer=buf, size=len(buf)) to be more readable? IMHO this example is why we shouldn't implement any form of assignment expressions in Python :) Yury
On Wed, Apr 25, 2018 at 1:49 AM, Yury Selivanov
On Tue, Apr 24, 2018 at 11:34 AM, Steven D'Aprano
wrote: On Tue, Apr 24, 2018 at 11:05:57AM -0400, Yury Selivanov wrote:
Well, `my_func(a=(b:=foo))` or `my_func(b:=foo)` are also barely readable to my eye.
There's no advantage to using binding-expressions unless you're going to re-use the name you just defined, and that re-use will give you a hint as to what is happening:
my_func(arg, buffer=(buf := [None]*get_size()), size=len(buf))
Again, this is very subjective, but this code would fail my code review :)
Don't you find
buf = [None] * get_size() my_func(arg, buffer=buf, size=len(buf))
to be more readable?
Only if 'buf' is going to be used elsewhere. I'd be looking down below for some other use of 'buf'. Technically the same could be true of the inline assignment, but it makes more sense for a "this statement only" name binding to be within that statement, not broken out and placed above it as another operation at equal importance. ChrisA
On Tue, Apr 24, 2018 at 11:58 AM, Chris Angelico
On Wed, Apr 25, 2018 at 1:49 AM, Yury Selivanov
wrote: On Tue, Apr 24, 2018 at 11:34 AM, Steven D'Aprano
wrote: [..] There's no advantage to using binding-expressions unless you're going to re-use the name you just defined, and that re-use will give you a hint as to what is happening:
my_func(arg, buffer=(buf := [None]*get_size()), size=len(buf))
Again, this is very subjective, but this code would fail my code review :)
Don't you find
buf = [None] * get_size() my_func(arg, buffer=buf, size=len(buf))
to be more readable?
Only if 'buf' is going to be used elsewhere. I'd be looking down below for some other use of 'buf'. Technically the same could be true of the inline assignment, but it makes more sense for a "this statement only" name binding to be within that statement, not broken out and placed above it as another operation at equal importance.
Well, you can use empty lines to visually indicate that 'buf' is related to the call. Moreover, 'buf' is still available to the code below that code and sometimes be used there. You can't tell for sure until you glance over the entire file/function. PEP 572 does not implement any sort of sub-scoping. Yury
On Wed, Apr 25, 2018 at 12:54 AM, Anthony Flury via Python-Dev
As discussed previously by others on this exact proposals, you now have the issue of confusion when using keyword arguments : *my_func(a = b)* : clearly that is a call to `my_func' where argument a has the value of b, but if you want to do an assigment expression when calling the function you now have to do *my_func((a=b)) -* which frankly looks messy in my opinion; you get the same issue when you are wanting to do assignment expressions in tuples.
To be fair, function arguments already follow "practicality beats purity" in many ways. Let's look at tuples: x = 1, 2 # fine x = (1, 2) # fine x = 1, # fine, though not advisable x = (1,) # fine But if you're going to use a tuple literal as a function parameter, you have to give it extra parens: f((1, 2)) # one arg, a tuple f(1, 2) # two args The comma has multiple meanings, and it has to be disambiguated. The equals sign would be the same. I'm still strongly -1 on any proposal to have "=" mean assignment in any expression context, though. It is WAY too easy for a comparison to sneakily become an assignment, or to get bizarre syntax errors: x = 1 if (x = 2): ... This, according to your proposal, raises SyntaxError - not because a comparison was wanted and an assignment was made, but because the name already had a value. And, even worse, this is NOT an error: x = 1 def f(): if (x = 2): ... That's a bizarre distinction. ChrisA
On Tue, Apr 24, 2018 at 11:07 AM, Chris Angelico
x = 1 if (x = 2): ...
This, according to your proposal, raises SyntaxError - not because a comparison was wanted and an assignment was made, but because the name already had a value. And, even worse, this is NOT an error:
Yes, because I'm trying to think about this from a pragmatic side of things. My question to myself: "what syntax could I use that would prevent me from making '=' vs '==' mistake when I code?" To me, the answer is that I usually want to compare local variables. When I compare to variables from outer scopes they *usually* are on the *right* side of '=='.
x = 1 def f(): if (x = 2): ...
That's a bizarre distinction.
Chris, FWIW I'm trying to avoid using 'bizarre', 'arcane' etc with regards to PEP 572 or any proposal, really. For example, I, personally, find ':=' bizarre, but it's subjective and it's unproductive to say that. Yury
On 04/24/2018 08:19 AM, Yury Selivanov wrote:
Yes, because I'm trying to think about this from a pragmatic side of things. My question to myself: "what syntax could I use that would prevent me from making '=' vs '==' mistake when I code?" To me, the answer is that I usually want to compare local variables.
I think we need to disambiguate between typo-typos and thinko-typos. I suspect the vast majority of the '=' bugs are not due to the programmer /thinking/ the wrong operation, but of their hands/keyboards not /entering/ the right symbols; having a legal operator ("==") degrade into another legal operator ("=") that looks similar but means incredibly different things is a trap that we should not add to Python. You might say that we have the same problems with ">=", "<=", and "!=". We don't with "!=" because neither "!" nor "=" can stand alone and would fail. We only have it partially with "<=" and ">=" because missing the angle bracket results in failure, but missing the "=" results in a working statement -- but that statement is still the same type of operation and is easier to debug when boundary cases fail.
When I compare to variables from outer scopes they *usually* are on the *right* side of '=='.
You mean something like if 2 == x: ? I never write code like that, and I haven't seen it, either. -- ~Ethan~
On Tue, Apr 24, 2018 at 11:51 AM, Ethan Furman
When I compare to variables from outer scopes they *usually* are on the *right* side of '=='.
You mean something like
if 2 == x:
? I never write code like that, and I haven't seen it, either.
Hm. I mean this: const = 'something' def foo(arg): if arg == const: do something Note that "const" is on the right side of "==". Would you write this as def foo(arg): if const == arg: ? ;) Yury
On 04/24/2018 08:56 AM, Yury Selivanov wrote:
On Tue, Apr 24, 2018 at 11:51 AM, Ethan Furman
wrote: When I compare to variables from outer scopes they *usually* are on the *right* side of '=='.
You mean something like
if 2 == x:
? I never write code like that, and I haven't seen it, either.
Hm. I mean this:
const = 'something'
def foo(arg): if arg == const: do something
Note that "const" is on the right side of "==".
Would you write this as
def foo(arg): if const == arg:
? ;)
Heh, no. But I do write this: def wrapper(func, some_value): value_I_want = process(some_value) def wrapped(*args, **kwds): if value_I_want == 42: ... -- ~Ethan~
On Tue, Apr 24, 2018 at 12:03 PM, Ethan Furman
But I do write this:
def wrapper(func, some_value): value_I_want = process(some_value) def wrapped(*args, **kwds): if value_I_want == 42: ...
But this pattern is more rare than comparing local variables. That's the point I'm trying to use. Besides, to make it an assignment expression under my proposal you would need to use parens. Which makes it even less likely that you confuse '=' and '=='. Yury
On 24/04/18 17:11, Yury Selivanov wrote:
But I do write this:
def wrapper(func, some_value): value_I_want = process(some_value) def wrapped(*args, **kwds): if value_I_want == 42: ... But this pattern is more rare than comparing local variables. That's
On Tue, Apr 24, 2018 at 12:03 PM, Ethan Furman
wrote: [..] the point I'm trying to use. Besides, to make it an assignment expression under my proposal you would need to use parens. Which makes it even less likely that you confuse '=' and '=='.
Just because you wrap a set of character in parens doesn't mean that you wont potentially mistype what you should type inside the parens. The failure mode of in C : if (a = 3) do_something_with_a(a); Is Incredibly common even with very experienced developers - so much so that most linters flag it as a likely error, and I think gcc has an option to flag it as a warning - even though it is valid and very occasionally it is useful. Also many developers who come to Python from languages such as C will still place parens around conditionals - this means that a typo which will cause a Syntax Error in current versions, but would cause a potentially subtle bug under your implementation (unless you maintain the rule that you can't rebind currently bound names - which renders the whole idea useless in loops (as already discussed at length). I also still can't think of a single other Python construct where the semantics of an operator are explicitly modified by syntaxtic elements outside the operator. For mathematical operators, the surrounding parens modifies the grouping of the operators but not the semantics (* means *, it is just the operands which potentially change). You could argue that your proposal overloads the semantics of the parens (similar to how braces are overloaded to implement dictionaries and set literals), but I don't think that overloading the semantics of parens is good idea. If Python is going to do assignment expressions we shouldn't overload parens in my opinion - we should have a separate operator - doing this avoids needing to exclude rebinding, and makes such expressions considerably more useful. -- Anthony Flury email : *Anthony.flury@btinternet.com* Twitter : *@TonyFlury https://twitter.com/TonyFlury/*
On Wed, Apr 25, 2018 at 7:29 AM, Anthony Flury via Python-Dev
On 24/04/18 17:11, Yury Selivanov wrote:
On Tue, Apr 24, 2018 at 12:03 PM, Ethan Furman
wrote: [..] But I do write this:
def wrapper(func, some_value): value_I_want = process(some_value) def wrapped(*args, **kwds): if value_I_want == 42: ...
But this pattern is more rare than comparing local variables. That's the point I'm trying to use. Besides, to make it an assignment expression under my proposal you would need to use parens. Which makes it even less likely that you confuse '=' and '=='.
Just because you wrap a set of character in parens doesn't mean that you wont potentially mistype what you should type inside the parens. The failure mode of in C :
if (a = 3) do_something_with_a(a);
Is Incredibly common even with very experienced developers - so much so that most linters flag it as a likely error, and I think gcc has an option to flag it as a warning - even though it is valid and very occasionally it is useful.
Technically what you have there trips *two* dodgy conditions, and could produce either warning or both: 1) Potential assignment where you meant comparison 2) Condition is always true. The combination makes it extremely likely that this wasn't intended. It's more dangerous, though, when the RHS is a function call...
Also many developers who come to Python from languages such as C will still place parens around conditionals - this means that a typo which will cause a Syntax Error in current versions, but would cause a potentially subtle bug under your implementation (unless you maintain the rule that you can't rebind currently bound names - which renders the whole idea useless in loops (as already discussed at length).
Yuri, look how many of the python-dev readers have completely misinterpreted this "can't rebind" rule. I think that's a fairly clear indication that the rule is not going to be well understood by anyone who isn't extremely familiar with the way the language is parsed. I hesitate to say outright that it is a *bad rule*, but that's the lines I'm thinking along. ChrisA
On Tue, Apr 24, 2018 at 10:29:11PM +0100, Anthony Flury via Python-Dev wrote:
If Python is going to do assignment expressions we shouldn't overload parens in my opinion - we should have a separate operator - doing this avoids needing to exclude rebinding, and makes such expressions considerably more useful.
Exactly! Yury's suggestion to prohibit rebinding existing names is throwing away the baby with the bathwater. Using assignment-expression to over-write an existing variable is only a error when it is a mistake, done by accident. To satisfy the compiler, we have to invent new and unique names for what is conceptually the same variable being reused. Instead of simplifying your code, it will make it more complex. Despite his statement that there's only one assignment, there are actually two: assignment that allows rebinding, and assignment that sometimes doesn't. The semantic differences between Yury's = and = are actually *greater* than the differences between = and := -- Steve
On Wed, Apr 25, 2018 at 1:56 AM, Yury Selivanov
On Tue, Apr 24, 2018 at 11:51 AM, Ethan Furman
wrote: When I compare to variables from outer scopes they *usually* are on the *right* side of '=='.
You mean something like
if 2 == x:
? I never write code like that, and I haven't seen it, either.
Hm. I mean this:
const = 'something'
def foo(arg): if arg == const: do something
Note that "const" is on the right side of "==".
Would you write this as
def foo(arg): if const == arg:
? ;)
That's assuming the global is a constant. What if it's a mode-setting marker? def foo(arg): ... if output == "verbose" or (output != "quiet" and error_count): print("Arg foo'd", arg) print("Errors found:", error_count) Then I would definitely put the variable first. And I know a lot of people who would parenthesize the first condition in this. ChrisA
On 24Apr2018 08:51, Ethan Furman
When I compare to variables from outer scopes they *usually* are on the *right* side of '=='.
You mean something like
if 2 == x:
? I never write code like that, and I haven't seen it, either.
Just to this, I also never write code like that but I've certainly seen it
advocated.
I think the rationale was that it places the comparison value foremost in one's
mind, versus the name being tested. I'm not persuaded, but it is another
subjective situation.
Cheers,
Cameron Simpson
On 2018-04-24 23:32, Cameron Simpson wrote:
On 24Apr2018 08:51, Ethan Furman
wrote: When I compare to variables from outer scopes they *usually* are on the *right* side of '=='.
You mean something like
if 2 == x:
? I never write code like that, and I haven't seen it, either.
Just to this, I also never write code like that but I've certainly seen it advocated.
I think the rationale was that it places the comparison value foremost in one's mind, versus the name being tested. I'm not persuaded, but it is another subjective situation.
It's sometimes advocated in C/C++ code to help catch the inadvertent use of = instead of ==, but that's not a problem in Python.
On Tue, Apr 24, 2018 at 09:50:34AM -0400, Yury Selivanov wrote:
On Tue, Apr 24, 2018 at 9:46 AM, Nick Coghlan
wrote: On 24 April 2018 at 23:38, Yury Selivanov
wrote: I propose to use the following syntax for assignment expressions:
( NAME = expr )
I know that it was proposed before and this idea was rejected, because accidentally using '=' in place of '==' is a pain point in C/C++/JavaScript.
That said, I believe we can still use this syntax as long as we impose the following three restrictions on it:
1. Only NAME token is allowed as a single target.
2. Parenthesis are required.
There are many places where I would use parentheses even if they are not required, but being forced to use them when they're not and I don't want them is ugly. I also question why you think this will help prevent accidentally writing = when you meant == (equality). Have you never written something like this? if (x == y) or (a > b): ... Yes, I know the parens are not strictly needed, since the precedence of `or` is lower than the comparison operators. But still, especially for complex comparisons, a few extra (round) brackets can improve readability. So now we have: if (x = y) or (a > b): ... # oops But the biggest problem with this is that it is ambiguous to the human reader. At a glance, I'm likely to read x=y in an expression as equality. If I notice that it is a single = sign, I'm never going to be sure whether it was a mistake or intentional until I study the rest of the function minutely. The benefit of := is that if I see it, I can be pretty sure it was not a typo. It is hard to mistype == as := by accident, and they are visually distinct enough that I am not going to misread := as == .
3. Most importantly: it is *not* allowed to mask names in the current local scope.
That means you can't rebind existing variables. That means you can't rebind to the same variable in a loop. I believe that one of the most important use-cases for binding- expression syntax is while loops, like this modified example taken from PEP 572 version 3: while (data = sock.read()): print("Received data:", data) If you prohibit re-binding data, that prohibits cases like this, or even using it inside a loop: for value in sequence: process(arg, (item = expression), item+1) Your suggestion to prohibit rebinding variables effectively makes them so crippled as to be useless to me.
While I agree this would be unambiguous to a computer, I think for most humans it would be experienced as a confusing set of arcane and arbitrary rules about what "=" means in Python.
I respectfully disagree. There are no "arcane and confusing rules" about "=", it's rather simple:
"=" is always an assignment.
Why is this allowed? x = 1 # both are statement forms x = 2 but this is prohibited? x = 1 (x = 2) # no rebinding is allowed and even more confusing, this is allowed! (x = 1) # x doesn't exist yet, so it is allowed x = 2 # statement assignment is allowed to rebind By the way, the check for existing variables cannot help to be either incomplete or incorrect if you try to do it at compile time: from module import * (x = 2) # allowed or not allowed? If you don't like wild-card imports, how about this: if random.random() > 0.5: spam = 1 else: eggs = 1 (spam = 2) # allowed or not? no way to tell at compile time But doing the rebinding/shadowing check at runtime will slow down binding expressions, and lead to even more arcane and confusing results: it = iter("abc") while (obj = next(it)): print(obj) will print "a" on the first loop, but then raise an exception on the second time loop as obj now exists. -- Steve
On Tue, Apr 24, 2018 at 11:15 AM, Steven D'Aprano
3. Most importantly: it is *not* allowed to mask names in the current local scope.
That means you can't rebind existing variables. That means you can't rebind to the same variable in a loop.
No, it doesn't. The check is performed during compile phase, and Python does not unroll loops. Anyways, read below.
I believe that one of the most important use-cases for binding- expression syntax is while loops, like this modified example taken from PEP 572 version 3:
while (data = sock.read()): print("Received data:", data)
If you prohibit re-binding data, that prohibits cases like this, or even using it inside a loop:
for value in sequence: process(arg, (item = expression), item+1)
No it doesn't. symtable in Python works differently. I encourage you to test my reference implementation: py> for val in [1, 2, 3]: ... print((item=val), item+1) ... 1 2 2 3 3 4
Why is this allowed?
x = 1 # both are statement forms x = 2
but this is prohibited?
x = 1 (x = 2) # no rebinding is allowed
and even more confusing, this is allowed!
(x = 1) # x doesn't exist yet, so it is allowed x = 2 # statement assignment is allowed to rebind
These all are very limited code snippets that you're unlikely to see in real code. I can write (and I did in this thread) a bunch of examples of where PEP 572 is also inconsistent. Yury
On Tue, Apr 24, 2018 at 11:25:58AM -0400, Yury Selivanov wrote:
No, it doesn't. The check is performed during compile phase, and Python does not unroll loops. Anyways, read below.
What does unrolling loops have to do with anything? And besides, loop unrolling is an implementation detail -- maybe Python will unroll loops, maybe it won't. If you insist that the check is only done at compile time, then your description is wrong and your rule that "it is *not* allowed to mask names in the current local scope" is false. It *is* allowed to shadow names in the local scope, but only names that cannot be determined at compile-time. from math import * process(arg, (pi = 1), pi+1) # allowed That's more and worse complexity. And what about masking names in the class, nonlocal, global and builtin scopes? Even more complexity and inconsistent behaviour! def function(): global a a = b = 1 process(arg, (a = 2), a+1) # allowed process(arg, (b = 2), b+1) # not allowed
I believe that one of the most important use-cases for binding- expression syntax is while loops, like this modified example taken from PEP 572 version 3:
while (data = sock.read()): print("Received data:", data)
If you prohibit re-binding data, that prohibits cases like this, or even using it inside a loop:
for value in sequence: process(arg, (item = expression), item+1)
No it doesn't. symtable in Python works differently. I encourage you to test my reference implementation:
py> for val in [1, 2, 3]: ... print((item=val), item+1) ... 1 2 2 3 3 4
Then your description is false: the assignment in the second time around the loop is masking the value that was set the first time around the loop. I should be able to unroll the loop by hand, and the code should still work: val = 1 print((item=val), item+1) val = 2 print((item=val), item+1) val = 3 print((item=val), item+1) Does your reference implementation allow that? If not, then you have added yet another inconsistency and obscure rule to be learned: using assignment expressions will break loop unrolling even if you do it by hand. If it *does* allow that, then so much for your claim that you cannot mask existing variables. It can.
Why is this allowed?
x = 1 # both are statement forms x = 2
but this is prohibited?
x = 1 (x = 2) # no rebinding is allowed
and even more confusing, this is allowed!
(x = 1) # x doesn't exist yet, so it is allowed x = 2 # statement assignment is allowed to rebind
These all are very limited code snippets that you're unlikely to see in real code.
Oh come on now Yury, please be reasonable. They're only *sketches* of more realistic code. Of course I'm not actually going to write something like x = 1 (x = 2) but do you really need me to take the time and effort to come up with a more realistic (and therefore complex) example? Okay. # allowed mo = re.match(HEADER_PATTERN, string) if mo: process_header(mo) ... # much later mo = re.match(FOOTER_PATTERN, string) if mo: process_footer(no) # not allowed mo = re.match(HEADER_PATTERN, string) if mo: process_header(mo) ... # much later if (mo = re.match(FOOTER_PATTERN, string)): # SyntaxError process_footer(no) You stated that 'There are no "arcane and confusing rules" about "=", it's rather simple' but every time we look closely at it, the rules seem to get more arcane and confusing. - why is it okay to mask nonlocal, global, class and builtin names, but not local? - for module-level code, how is the compiler supposed to determine the local names in the face of wildcard imports? - why is it a syntax error to assign to a name which is not actually used? # not allowed if "a".upper() == "XYZ"[-1].lower(): spam = "this is dead code and will never happen" process(arg, (spam=expression), spam+1) # syntax error # but this is allowed if "a".upper() == "XYZ"[-1].lower(): spam = "this is dead code and will never happen" spam = expression process(arg, spam, spam+1) - why can you *sometimes* mask existing local variables, if they are used in a loop, but not *other* local variables? - how does this stop *me*, the human reader, from misreading (name=expression) as an equality test? -- Steve
On Wed, Apr 25, 2018 at 2:23 AM, Steven D'Aprano
On Tue, Apr 24, 2018 at 11:25:58AM -0400, Yury Selivanov wrote:
No, it doesn't. The check is performed during compile phase, and Python does not unroll loops. Anyways, read below.
What does unrolling loops have to do with anything? And besides, loop unrolling is an implementation detail -- maybe Python will unroll loops, maybe it won't.
If you insist that the check is only done at compile time, then your description is wrong and your rule that "it is *not* allowed to mask names in the current local scope" is false. It *is* allowed to shadow names in the local scope, but only names that cannot be determined at compile-time.
from math import * process(arg, (pi = 1), pi+1) # allowed
That's more and worse complexity.
That's not allowed at local scope (at least, it's not allowed at function scope - is there any other "local scope" at which it is allowed?). Not sure if you can craft equivalent shenanigans with 'exec'. ChrisA
On Wed, Apr 25, 2018 at 02:42:08AM +1000, Chris Angelico wrote:
from math import * process(arg, (pi = 1), pi+1) # allowed
That's not allowed at local scope (at least, it's not allowed at function scope - is there any other "local scope" at which it is allowed?).
Of course: local just means the current scope, wherever you happen to be. Inside a function, local is the current function scope. Inside a class body, local is the class body scope. At the top level of the module, local means the module scope (and locals() returns the same dict as globals()). If Yury means for this "cannot mask existing variables" to only operate inside functions, that means that you can mask existing variables if you use assignment expressions in class bodies or top-level module code. -- Steve
On Wed, Apr 25, 2018 at 2:57 AM, Steven D'Aprano
On Wed, Apr 25, 2018 at 02:42:08AM +1000, Chris Angelico wrote:
from math import * process(arg, (pi = 1), pi+1) # allowed
That's not allowed at local scope (at least, it's not allowed at function scope - is there any other "local scope" at which it is allowed?).
Of course: local just means the current scope, wherever you happen to be. Inside a function, local is the current function scope. Inside a class body, local is the class body scope. At the top level of the module, local means the module scope (and locals() returns the same dict as globals()).
If Yury means for this "cannot mask existing variables" to only operate inside functions, that means that you can mask existing variables if you use assignment expressions in class bodies or top-level module code.
I don't have a quote for it, but I was under the impression that this shielding was indeed function-scope-only. Actually, now that I think about it, I'm not sure whether Yuri's plan for assignment expressions even included module scope. ChrisA
On Wed, Apr 25, 2018 at 1:15 AM, Steven D'Aprano
By the way, the check for existing variables cannot help to be either incomplete or incorrect if you try to do it at compile time:
from module import * (x = 2) # allowed or not allowed?
If you don't like wild-card imports, how about this:
if random.random() > 0.5: spam = 1 else: eggs = 1 (spam = 2) # allowed or not? no way to tell at compile time
But doing the rebinding/shadowing check at runtime will slow down binding expressions, and lead to even more arcane and confusing results:
it = iter("abc") while (obj = next(it)): print(obj)
will print "a" on the first loop, but then raise an exception on the second time loop as obj now exists.
On re-thinking this, I think the distinction IS possible, but (a) only in function/class scope, not at global; and (b) would be defined in terms of lexical position, not run-time. For instance: def f(): (a = 1) # Legal; 'a' has not been used yet a = 2 # doesn't change that def f(a): (a = 1) # Invalid - 'a' has been used already def f(): while (a = get_next()): # Legal ... This could be handled in the symbol collection pass; if the name already exists in the function's locals, it's disallowed. But I still stand by my statement that this creates bizarre cases, and yes, I know that that word is subjective (just like "readable", "intuitive", and "sensible"). The rules as given sound like they would make great linter rules and terrible syntax rules. They are closely aligned with the OP's experience and usage patterns - which means that, as a personal linter, they could marvellously assist in catching bugs. You could have personal (or organization-wide) linter rules disallowing "class foo:" with a lower-case name, and disallowing the rebinding of any name in ALL_CAPS, but I would not want either rule codified into language syntax. ChrisA
On Tue, Apr 24, 2018 at 11:28 AM, Chris Angelico
On re-thinking this, I think the distinction IS possible, but (a) only in function/class scope, not at global; and (b) would be defined in terms of lexical position, not run-time. For instance:
def f(): (a = 1) # Legal; 'a' has not been used yet a = 2 # doesn't change that
def f(a): (a = 1) # Invalid - 'a' has been used already
def f(): while (a = get_next()): # Legal ...
Now *this* is a weird rule. Moving functions around files would become impossible. Please experiment with my reference implementation, it already implements my proposal in full. Loops and inline assignments work as expected in it. Yury
On Tue, 24 Apr 2018 23:46:34 +1000
Nick Coghlan
On 24 April 2018 at 23:38, Yury Selivanov
wrote: I propose to use the following syntax for assignment expressions:
( NAME = expr )
I know that it was proposed before and this idea was rejected, because accidentally using '=' in place of '==' is a pain point in C/C++/JavaScript.
That said, I believe we can still use this syntax as long as we impose the following three restrictions on it:
1. Only NAME token is allowed as a single target.
2. Parenthesis are required.
3. Most importantly: it is *not* allowed to mask names in the current local scope.
While I agree this would be unambiguous to a computer, I think for most humans it would be experienced as a confusing set of arcane and arbitrary rules about what "=" means in Python.
If the ambition is to find a piece of syntax that reads as "binds", then we can use a variation on the FLUFL operator: "<->". Regards Antoine.
On 24 April 2018 at 14:46, Nick Coghlan
On 24 April 2018 at 23:38, Yury Selivanov
wrote: I propose to use the following syntax for assignment expressions:
( NAME = expr )
I know that it was proposed before and this idea was rejected, because accidentally using '=' in place of '==' is a pain point in C/C++/JavaScript.
That said, I believe we can still use this syntax as long as we impose the following three restrictions on it:
1. Only NAME token is allowed as a single target.
2. Parenthesis are required.
3. Most importantly: it is *not* allowed to mask names in the current local scope.
While I agree this would be unambiguous to a computer, I think for most humans it would be experienced as a confusing set of arcane and arbitrary rules about what "=" means in Python.
Also, there's the ambiguity and potential for misreading in the opposite direction (accidentally *reading* = as == even though it isn't): if (diff = x - x_base) and (g = gcd(diff, n)) > 1: return g My immediate reading of this is as an equality comparison between diff and x - x_base (which would send me futilely looking for a definition of diff) and an equality comparison of g and gcd(diff, n)... wait, why am I doing (equality comparison) > 1? Oh, hang on... At this point, any hope of me quickly understanding what this code does is lost. Paul
On Tue, Apr 24, 2018 at 10:49 AM, Paul Moore
3. Most importantly: it is *not* allowed to mask names in the current local scope.
While I agree this would be unambiguous to a computer, I think for most humans it would be experienced as a confusing set of arcane and arbitrary rules about what "=" means in Python.
Also, there's the ambiguity and potential for misreading in the opposite direction (accidentally *reading* = as == even though it isn't):
if (diff = x - x_base) and (g = gcd(diff, n)) > 1: return g
Since 'diff' and 'g' must be new names according to rule (3), those who read the code will notice that both were not previously bound. Therefore both are new variables so it can't be a comparison. Yury
On 24 April 2018 at 15:58, Yury Selivanov
On Tue, Apr 24, 2018 at 10:49 AM, Paul Moore
wrote: [..] 3. Most importantly: it is *not* allowed to mask names in the current local scope.
While I agree this would be unambiguous to a computer, I think for most humans it would be experienced as a confusing set of arcane and arbitrary rules about what "=" means in Python.
Also, there's the ambiguity and potential for misreading in the opposite direction (accidentally *reading* = as == even though it isn't):
if (diff = x - x_base) and (g = gcd(diff, n)) > 1: return g
Since 'diff' and 'g' must be new names according to rule (3), those who read the code will notice that both were not previously bound. Therefore both are new variables so it can't be a comparison.
That was essentially my point, though - I can no longer read that line of code in isolation from the surrounding context. Consider something like a github PR review screen, where surrounding unchanged code is frequently hidden. Anyway, we can agree to differ on this - I don't like this idea and I'd personally find it hard to read, but as you've already pointed out, this is all extremely subjective. Paul
On Wed, Apr 25, 2018 at 12:58 AM, Yury Selivanov
On Tue, Apr 24, 2018 at 10:49 AM, Paul Moore
wrote: [..] 3. Most importantly: it is *not* allowed to mask names in the current local scope.
While I agree this would be unambiguous to a computer, I think for most humans it would be experienced as a confusing set of arcane and arbitrary rules about what "=" means in Python.
Also, there's the ambiguity and potential for misreading in the opposite direction (accidentally *reading* = as == even though it isn't):
if (diff = x - x_base) and (g = gcd(diff, n)) > 1: return g
Since 'diff' and 'g' must be new names according to rule (3), those who read the code will notice that both were not previously bound. Therefore both are new variables so it can't be a comparison.
That would not be true if this code were in a loop. Or do you have a different definition of "not previously bound" that is actually a syntactic feature? For instance: if (x = 1): x = 2 Legal? Not legal? ChrisA
On Tue, Apr 24, 2018 at 10:58:24AM -0400, Yury Selivanov wrote:
Since 'diff' and 'g' must be new names according to rule (3), those who read the code will notice that both were not previously bound.
How am I supposed to notice that they've never been bound without carefully reading through the rest of the function in detail, checking every single expression and statement? And besides, you have already established that there are exceptions to the rule "names must be new names". For example, in loops. What other exceptions are there? -- Steve
On Wed, Apr 25, 2018 at 2:28 AM, Steven D'Aprano
On Tue, Apr 24, 2018 at 10:58:24AM -0400, Yury Selivanov wrote:
Since 'diff' and 'g' must be new names according to rule (3), those who read the code will notice that both were not previously bound.
How am I supposed to notice that they've never been bound without carefully reading through the rest of the function in detail, checking every single expression and statement?
And besides, you have already established that there are exceptions to the rule "names must be new names". For example, in loops.
What other exceptions are there?
Yuri is talking about "new" in the syntactic sense. A new name is one which, reading lexically through the code, has not yet been assigned to in the function. (I don't know what happens with global/nonlocal declarations.) Loops have a single point at which the name is assigned to. This has a single point where the name is assigned, too, even though you'll never hit it: def f(x): if x is not x: y = 1 print(y) # UnboundLocalError While I disagree with the proposal, it is at least sane from the compiler's POV. I don't think it makes sense from a human's POV, but it's internally consistent. ChrisA
Thanks for thinking this through, Yury. :)
FWIW, I'm still unconvinced that an assignment expression is worth it.
It's hard to say, though, without seeing how much folks would actually
use it (and I don't have my own time machine unfortunately). IIRC, in
the past several proposed syntax (e.g. decorators) were in the same
boat and in retrospect turned out to be a strongly positive addition
to the language. :)
Comments in-line below.
-eric
On Tue, Apr 24, 2018 at 7:38 AM, Yury Selivanov
I propose to use the following syntax for assignment expressions:
( NAME = expr )
[snip]
1. Only NAME token is allowed as a single target.
This seems reasonable and does keep assignment expressions easier for the reader to find. At the same time, there have been some arguments elsewhere in favor of tuple unpacking as the other obvious use case. Presumably that would not be supported under this rule.
2. Parenthesis are required.
Similar to rule #1, this would make assignment expressions more obvious to readers, which is a good thing.
3. Most importantly: it is *not* allowed to mask names in the current local scope.
I was about to leave this counter example for accidentally-typed-one-equal-sign-instead-of-two bugs: if (y == 3): print(y) # vs. if (y = 3): print(y) Then it dawned on me that your rule #3 solves this. :)
[snip]
py> f = lambda x: x * 10 py> [[(y = f(x)), x/y] for x in range(1,5)] [[10, 0.1], [20, 0.1], [30, 0.1], [40, 0.1]]
[snip]
I believe that this syntax is the best of both worlds: it allows to write succinct code just like PEP 572, but without introducing a new ':=' operator.
This is the main point of this alternate proposal, right? It certainly seems reasonable that we not add another assignment syntax. On the other hand, having ":=" as a distinct syntax for assignment expressions is close enough to the existing syntax that it doesn't really add any real extra burden to readers, while being more searchable and visually distinct. If we were to add assignment expressions I'd probably favor ":=". Regardless, your 3 rules would benefit either syntax. Nick may have a point that the rules might be an excessive burden, but I don't think it's too big a deal since the restrictions are few (and align with the most likely usage) and are limited to syntax so the compiler will be quick to point mistakes.
On 25 April 2018 at 00:54, Eric Snow
Regardless, your 3 rules would benefit either syntax. Nick may have a point that the rules might be an excessive burden, but I don't think it's too big a deal since the restrictions are few (and align with the most likely usage) and are limited to syntax so the compiler will be quick to point mistakes.
I think the "single name target only" rule should be in place no matter the syntax for the name binding operator itself. I don't mind too much either way on the mandatory parentheses question (it's certainly an easy option to actively discourage use of binding expressions as a direct alternative to assignment statements, but as with the single-name-only rule, it's independent of the choice of syntax) I *do* think the "no name rebinding except in a while loop header" restriction would be annoying for the if/elif use case and the while use case: while (item = get_item()) is not first_delimiter: # First processing loop while (item = get_item()) is not second_delimiter: # Second processing loop # etc... if (target = get_first_candidate()) is not None: ... elif (target = get_second_candidate()) is not None: ... elif (target = get_third_candidate()) is not None: ... And *that* rule is unique to the "=" spelling, since for other proposals "lhs = rhs" in an expression is *always* a syntax error, and you have to resolve the ambiguity in intent explicitly by either adding a second "=" (to request equality comparison), or else some other leading symbol (to request a binding expression). Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia
On Tue, Apr 24, 2018 at 11:31 AM, Nick Coghlan
On 25 April 2018 at 00:54, Eric Snow
wrote: Regardless, your 3 rules would benefit either syntax. Nick may have a point that the rules might be an excessive burden, but I don't think it's too big a deal since the restrictions are few (and align with the most likely usage) and are limited to syntax so the compiler will be quick to point mistakes.
I think the "single name target only" rule should be in place no matter the syntax for the name binding operator itself.
I don't mind too much either way on the mandatory parentheses question (it's certainly an easy option to actively discourage use of binding expressions as a direct alternative to assignment statements, but as with the single-name-only rule, it's independent of the choice of syntax)
Mandatory parenthesis around `(name := expr)` would at least solve the problem of users mixing up '=' and ':=' in statements.
I *do* think the "no name rebinding except in a while loop header" restriction would be annoying for the if/elif use case and the while use case:
while (item = get_item()) is not first_delimiter: # First processing loop while (item = get_item()) is not second_delimiter: # Second processing loop # etc...
if (target = get_first_candidate()) is not None: ... elif (target = get_second_candidate()) is not None: ... elif (target = get_third_candidate()) is not None: ...
Yes, it would force users to come up with better names *iff* they want to use this new sugar: if (first_target = get_first_candidate()) ... elif (second_target = get_second_candidate()) ... Yury
On 25 April 2018 at 01:35, Yury Selivanov
On Tue, Apr 24, 2018 at 11:31 AM, Nick Coghlan
wrote: I *do* think the "no name rebinding except in a while loop header" restriction would be annoying for the if/elif use case and the while use case:
while (item = get_item()) is not first_delimiter: # First processing loop while (item = get_item()) is not second_delimiter: # Second processing loop # etc...
if (target = get_first_candidate()) is not None: ... elif (target = get_second_candidate()) is not None: ... elif (target = get_third_candidate()) is not None: ...
Yes, it would force users to come up with better names *iff* they want to use this new sugar:
if (first_target = get_first_candidate()) ... elif (second_target = get_second_candidate()) ...
Sorry, I didn't make the intended nature of that example clear: if (target = get_first_candidate()) is not None: ... # Any setup code specific to this kind of target elif (target = get_second_candidate()) is not None: ... # Any setup code specific to this kind of target elif (target = get_third_candidate()) is not None: ... # Any setup code specific to this kind of target else: raise RuntimeError("No valid candidate found") # Common code using target goes here Using a separate name in each branch wouldn't solve the problem of binding the *same* name for the common code to use later - you'd have to put a "target = candidate_n" bit of boilerplate in each branch. Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia
On 04/24/2018 08:35 AM, Yury Selivanov wrote:
Yes, it would force users to come up with better names *iff* they want to use this new sugar:
if (first_target = get_first_candidate()) ... elif (second_target = get_second_candidate()) ...
And then the common code below that only cares about a target being found now has to do a item = first_target or second_target or ... I don't see that as an improvement. -- ~Ethan~
On Tue, Apr 24, 2018 at 11:35:20AM -0400, Yury Selivanov wrote:
Yes, it would force users to come up with better names *iff* they want to use this new sugar:
if (first_target = get_first_candidate()) ... elif (second_target = get_second_candidate()) ...
They're not better names. Adding "first_" and "second_" prefixes are just meaningless waffle added to the name "target" to satisfy the compiler so it doesn't complain about reusing the name. And it is a clear inconsistency with = as a statement and = as an expression: # allowed target = get_first_candidate() if target: ... else: target = get_second_candidate() if target: ... # refactor, and we get a syntax error if (target = get_first_candidate()): ... elif (target = get_second_candidate()): ... And I cannot even begin to guess whether this will be allowed or not: if (target = get_first_candidate()): ... while (target = get_second_candidate()): ... -- Steve
Yury Selivanov wrote:
I propose to use the following syntax for assignment expressions:
( NAME = expr )
Yury, did you notice the subthread [1] that discusses the merits and problems of the same idea (except for your point 3)? [1] https://mail.python.org/pipermail/python-dev/2018-April/152868.html
On Tue, 24 Apr 2018 09:38:33 -0400
Yury Selivanov
I propose to use the following syntax for assignment expressions:
( NAME = expr )
I know that it was proposed before and this idea was rejected, because accidentally using '=' in place of '==' is a pain point in C/C++/JavaScript.
To solve this issue, I would suggest another syntax: var NAME = expr Strong points: - the "var" keyword makes it clear that it's not a mistyped equality ("var NAME == expr" would be a syntax error) - the "var" keyword can stand out thanks to syntax highlighting - the "=" which traditionally spells assignement is there as well Weak points: - we need a deprecation cycle before "var" can be used as a keyword (alternative keyword choices against "var": "using", "let", "bind"...) Regards Antoine.
We should really take this back to python-ideas at this point.
On Tue, Apr 24, 2018 at 3:16 PM, Antoine Pitrou
On Tue, 24 Apr 2018 09:38:33 -0400 Yury Selivanov
wrote: I propose to use the following syntax for assignment expressions:
( NAME = expr )
I know that it was proposed before and this idea was rejected, because accidentally using '=' in place of '==' is a pain point in C/C++/JavaScript.
To solve this issue, I would suggest another syntax:
var NAME = expr
Strong points: - the "var" keyword makes it clear that it's not a mistyped equality ("var NAME == expr" would be a syntax error) - the "var" keyword can stand out thanks to syntax highlighting - the "=" which traditionally spells assignement is there as well
Weak points: - we need a deprecation cycle before "var" can be used as a keyword
(alternative keyword choices against "var": "using", "let", "bind"...)
Regards
Antoine.
_______________________________________________ Python-Dev mailing list Python-Dev@python.org https://mail.python.org/mailman/listinfo/python-dev Unsubscribe: https://mail.python.org/mailman/options/python-dev/ guido%40python.org
-- --Guido van Rossum (python.org/~guido)
On Tue, Apr 24, 2018 at 03:54:30PM -0700, Guido van Rossum wrote:
We should really take this back to python-ideas at this point.
Please no :-( Variants of "let" etc were discussed earlier and didn't seem to get much interest. Although I don't think "var" specifically was suggested before, "let" certainly has been: https://mail.python.org/pipermail/python-ideas/2018-February/049031.html In the same thread, I also independently suggested "let": https://mail.python.org/pipermail/python-ideas/2018-February/048981.html This suggestion didn't seem to get much in the way of support, so much so that in the hundreds of emails for PEP 572 the idea seems to have been completely forgotten until now. Perhaps it ought to be added to the PEP in the rejected syntax section. I don't think there's anything to gain by rehashing this again: whether we choose "let", "var" or "bind", there's no real difference. It's still a new keyword, and it still looks like little else in Python today: while (var spam = expression) and spam in values: ... It's not really awful, but nor is it clearly better than := and the fact that it will require a keyword is a major point against it. I'm speaking for myself, of course, not Chris, but while consensus is of course desirable I think there comes a time where the PEP author is entitled to say "I've heard enough arguments, and I'm not convinced by them, this is my proposal and here it stays". If it were my PEP, at this point I would say that if anyone wants to champion an alternative syntax they can write their own PEP :-) I think this idea is going to be too divisive to expect a consensus to emerge, rather like the ternary if operator. I truly believe we've covered just about everything that needs covering, over many hundreds of emails in multiple threads, and unless somebody puts their hand up to write a competing PEP (or at least says something new beyond what we've already seen many times before) I think it is just about time for a BDFL decision between: 1. Reject the PEP. Not going to happen. 2. Leave the PEP pending. Maybe some time in the future. 3. Accept the PEP using the `name := expression` syntax. My preference would be for 3, even though := isn't my first choice for syntax, it's still a good choice. I think there are sufficient use-cases to justify this feature, including some long standing annoyances such as the dance we have to play with regexes, which only gets more annoying as you add more alternatives and cascading indentation. mo = re.match(pattern1, string) if mo: process(mo) else: mo = re.match(pattern2, string) if mo: handle(mo) else: mo = ... # you get the picture (I've changed my mind from earlier when I said this didn't count as a separate use-case from those already given. I think that cascading regexes is a big enough pain that it should be explicitly listed in the PEP as a motivation.) -- Steve
On Wed, Apr 25, 2018 at 2:27 AM, Steve Holden
On Wed, Apr 25, 2018 at 6:16 AM, Steven D'Aprano
wrote: On Tue, Apr 24, 2018 at 03:54:30PM -0700, Guido van Rossum wrote:
We should really take this back to python-ideas at this point.
Please no :-(
+1
Maybe I should have just said "no". Thanks Steven D'A for saving everyone yet another detour on this proposal. I'm pretty excited myself about NAME := <expression> and am mostly ignoring the current crop of counter-proposal. -- --Guido van Rossum (python.org/~guido)
On Thu, Apr 26, 2018 at 1:46 AM, Guido van Rossum
On Wed, Apr 25, 2018 at 2:27 AM, Steve Holden
wrote: On Wed, Apr 25, 2018 at 6:16 AM, Steven D'Aprano
wrote: On Tue, Apr 24, 2018 at 03:54:30PM -0700, Guido van Rossum wrote:
We should really take this back to python-ideas at this point.
Please no :-(
+1
Maybe I should have just said "no". Thanks Steven D'A for saving everyone yet another detour on this proposal.
I'm pretty excited myself about NAME := <expression> and am mostly ignoring the current crop of counter-proposal.
Of course, if someone wants to start a python-bad-ideas mailing list, I'm sure some of these alternatives would be perfect for it... If anyone hasn't seen the latest iteration of the PEP, I recently re-posted it. And it's always available here: https://www.python.org/dev/peps/pep-0572/ ChrisA
participants (16)
-
Anthony Flury
-
Antoine Pitrou
-
Barry Warsaw
-
Cameron Simpson
-
Chris Angelico
-
Christoph Groth
-
Eric Snow
-
Ethan Furman
-
Greg Ewing
-
Guido van Rossum
-
MRAB
-
Nick Coghlan
-
Paul Moore
-
Steve Holden
-
Steven D'Aprano
-
Yury Selivanov