(name := expression) doesn't fit the narrative of PEP 20
PEP 572 caused a strong emotional reaction in me. I wanted to first understand my intuitive objection to the idea before posting anything. I feel that (name := expression) doesn't fit the narrative of PEP 20. It doesn't remove complexity, it only moves it. What was its own assignment before now is part of the logic test. This saves on vertical whitespace but makes parsing and understanding logic tests harder. This is a bad bargain: logic tests already contain a lot of complexity that human readers have to cope with. Proponents of := argue it makes several patterns flatter (= better than nested) to express. Serial regular expression matching is a popular example. However, (name := expression) itself is making logic tests more nested, not flatter. It makes information in the logic test denser (= worse than sparse). Since it also requires an additional pair of parentheses, it forces the reader to decompose the expression in their head. := also goes against having one obvious way to do it. Since it's an expression, it can also be placed on its own line or in otherwise weird places like function call arguments. I anticipate PEP 8 would have to be extended to explicitly discourage such abuse. Linters would grow rules against it. This is noise. I'm -1 on PEP 572, I think it's very similar in spirit to the rejected PEP 463. -- Ł
On Thu, Apr 26, 2018 at 6:21 AM, Łukasz Langa <lukasz@langa.pl> wrote:
:= also goes against having one obvious way to do it. Since it's an expression, it can also be placed on its own line or in otherwise weird places like function call arguments. I anticipate PEP 8 would have to be extended to explicitly discourage such abuse. Linters would grow rules against it. This is noise.
Does this argument also apply to the if/else expression? Do linters need rules to advise against people writing code like: print(x) if x is None else print(y) ? It's perfectly legal to write code like this. But I don't see people abusing this sort of thing. ChrisA
On 25 Apr, 2018, at 1:24 PM, Chris Angelico <rosuav@gmail.com> wrote:
On Thu, Apr 26, 2018 at 6:21 AM, Łukasz Langa <lukasz@langa.pl> wrote:
:= also goes against having one obvious way to do it. Since it's an expression, it can also be placed on its own line or in otherwise weird places like function call arguments. I anticipate PEP 8 would have to be extended to explicitly discourage such abuse. Linters would grow rules against it. This is noise.
Does this argument also apply to the if/else expression? Do linters need rules to advise against people writing code like:
print(x) if x is None else print(y)
? It's perfectly legal to write code like this. But I don't see people abusing this sort of thing.
Ternary expressions are different because their flow is deliberately different from a regular if statement. It's also different from the C equivalent. `:=` on the other hand is deciptively similar to `=`. But yeah, I think worrying about abuse of the feature is a red herring. The gist of my criticism of your PEP is about the decreased balance in information density. -- Ł
A very emotional appeal, you don't seem to grasp the usability improvements this will give. I hear you but at this point appeals to Python's "Zen" don't help you. On Wed, Apr 25, 2018 at 1:21 PM, Łukasz Langa <lukasz@langa.pl> wrote:
PEP 572 caused a strong emotional reaction in me. I wanted to first understand my intuitive objection to the idea before posting anything.
I feel that (name := expression) doesn't fit the narrative of PEP 20. It doesn't remove complexity, it only moves it. What was its own assignment before now is part of the logic test. This saves on vertical whitespace but makes parsing and understanding logic tests harder. This is a bad bargain: logic tests already contain a lot of complexity that human readers have to cope with.
Proponents of := argue it makes several patterns flatter (= better than nested) to express. Serial regular expression matching is a popular example. However, (name := expression) itself is making logic tests more nested, not flatter. It makes information in the logic test denser (= worse than sparse). Since it also requires an additional pair of parentheses, it forces the reader to decompose the expression in their head.
:= also goes against having one obvious way to do it. Since it's an expression, it can also be placed on its own line or in otherwise weird places like function call arguments. I anticipate PEP 8 would have to be extended to explicitly discourage such abuse. Linters would grow rules against it. This is noise.
I'm -1 on PEP 572, I think it's very similar in spirit to the rejected PEP 463.
-- Ł
_______________________________________________ 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 25 Apr, 2018, at 1:28 PM, Guido van Rossum <guido@python.org> wrote:
You don't seem to grasp the usability improvements this will give. I hear you but at this point appeals to Python's "Zen" don't help you.
This reads dismissive to me. I did read the PEP and followed the discussion on python-dev. I referred to PEP 20 because it distills what's unique about the value proposition of Python. It's our shared vocabulary. Can you address the specific criticism I had? To paraphrase it without PEP 20 jargon:
(name := expression) makes code less uniform. It inserts more information into a place that is already heavily packed with information (logic tests).
-- Ł
[Guido]
You don't seem to grasp the usability improvements this will give. I hear you but at this point appeals to Python's "Zen" don't help you.
[Łukasz Langa <lukasz@langa.pl>]
This reads dismissive to me. I did read the PEP and followed the discussion on python-dev. I referred to PEP 20 because it distills what's unique about the value proposition of Python. It's our shared vocabulary.
Can you address the specific criticism I had? To paraphrase it without PEP 20 jargon:
(name := expression) makes code less uniform. It inserts more information into a place that is already heavily packed with information (logic tests).
I'll take a crack at that. It's not about "head arguments" at all. I sat out the first hundred messages about this on python-ideas, and looked at code instead. What I found had little to do with any of the head (abstract) arguments passionately debated for the duration ;-) In real life, I found a great many conditional tests that not only weren't "heavily packed" with information, they were simply of the form: NAME = expression if NAME: ... use NAME ... That looks more like assembly language than Python ;-) I saw no harm at all, and a little gain, in if NAME := expression: ... use NAME ... instead. But even a little gain adds up when it happens so often. Of course there have been better examples given of bigger gains. But in no case have the tests in those examples been "heavily packed with information". If they had been, I would have suggested instead breaking the test clauses _out_ of the conditional statements, and giving them names each on their own dedicated lines, with comments explaining what the heck the _intents_ are, even at the cost of adding an indentation level or two. Sometimes conditionals are _already_ "too dense". But more often they're very sparse. This becomes a question of seasoned judgment. For example, here's a real loop summing a series expansion, until the new terms become so small they make no difference to the running total (a common enough pattern in code slinging floats or decimals): while True: old = total total += term if old == total: return total term *= mx2 / (i*(i+1)) i += 2 To my eyes, this is genuinely harder to follow, despite its relative brevity: while total != (total := total + term): term *= mx2 / (i*(i+1)) i += 2 return total So I wouldn't use binding expressions in that case. I don't have a compelling head argument for _why_ I find the latter spelling harder to follow, but I don't need a theory to know that I in fact do. But neither do I need a compelling head argument for "why" to know that in many other cases I find that the use of binding expressions improves the code. You shouldn't believe me even if I pretended to have one and passionately argued for it. But, by the same token, I'm spectacularly unmoved by other peoples' head arguments. For that reason, the messages that sway me are those showing real code, or at least plausibly realistic code. In the majority of those so far, binding expressions would be a small-to-major win.
On Wed, 25 Apr 2018 16:55:43 -0500 Tim Peters <tim.peters@gmail.com> wrote:
To my eyes, this is genuinely harder to follow, despite its relative brevity:
while total != (total := total + term):
Does it even work? Perhaps if the goal is to stop when total is NaN, but otherwise?
For that reason, the messages that sway me are those showing real code, or at least plausibly realistic code. In the majority of those so far, binding expressions would be a small-to-major win.
I'm sure it's possible to find thousands of line of code where binding expressions wouldn't be a win, but I'm not sure that would be a constructive use of mailing-list bandwidth. Regards Antoine.
[Tim]
To my eyes, this is genuinely harder to follow, despite its relative brevity:
while total != (total := total + term):
[Antoine]
Does it even work? Perhaps if the goal is to stop when total is NaN, but otherwise?
I don't follow you. You snipped all the text explaining why it would work, so trying reading that again? When, e.g., `total` reaches 1.0 and `term` reaches 1e-30, this becomes: while 1.0 != (total := 1.0 + 1-e30): which leaves `total` unchanged (1.0 + 1e-30 == 1.0) and then while 1.0 != 1.0: causes the loop to exit (`while False:`).
For that reason, the messages that sway me are those showing real code, or at least plausibly realistic code. In the majority of those so far, binding expressions would be a small-to-major win.
I'm sure it's possible to find thousands of line of code where binding expressions wouldn't be a win, but I'm not sure that would be a constructive use of mailing-list bandwidth.
And that "argument" is? ;-) Note that I managed to move the PEP _away_ from general "assignment expressions" to the much simpler "binding expressions" precisely _by_ illustrating, via real code, why the generality of the former wasn't actually useful in any case I looked at. If something is always - or almost always - useless, that can be shown via considering realistic code. That was far more productive than endless abstract debates.
On Thu, Apr 26, 2018 at 8:08 AM, Antoine Pitrou <solipsis@pitrou.net> wrote:
On Wed, 25 Apr 2018 16:55:43 -0500 Tim Peters <tim.peters@gmail.com> wrote:
To my eyes, this is genuinely harder to follow, despite its relative brevity:
while total != (total := total + term):
Does it even work? Perhaps if the goal is to stop when total is NaN, but otherwise?
Yes, it does, because the first "total" is looked up before the rebinding happens. It's 100% unambiguous to the compiler... but still pretty unclear to a human. And I think the multiple use of 'total' is to blame for that. So I agree with Tim that this particular example is better in longhand. ChrisA
On Thu, 26 Apr 2018 08:38:51 +1000 Chris Angelico <rosuav@gmail.com> wrote:
On Thu, Apr 26, 2018 at 8:08 AM, Antoine Pitrou <solipsis@pitrou.net> wrote:
On Wed, 25 Apr 2018 16:55:43 -0500 Tim Peters <tim.peters@gmail.com> wrote:
To my eyes, this is genuinely harder to follow, despite its relative brevity:
while total != (total := total + term):
Does it even work? Perhaps if the goal is to stop when total is NaN, but otherwise?
Yes, it does, because the first "total" is looked up before the rebinding happens. It's 100% unambiguous to the compiler... but still pretty unclear to a human. And I think the multiple use of 'total' is to blame for that. So I agree with Tim that this particular example is better in longhand.
"Better" is an understatement :-( Now that I understood it (thanks for the explanation), the shorthand version appears completely bonkers. Regards Antoine.
[Tim]
To my eyes, this is genuinely harder to follow, despite its relative brevity:
while total != (total := total + term):
[Antoine]
Does it even work? Perhaps if the goal is to stop when total is NaN, but otherwise?
[Chris]
Yes, it does, because the first "total" is looked up before the rebinding happens. It's 100% unambiguous to the compiler... but still pretty unclear to a human. And I think the multiple use of 'total' is to blame for that. So I agree with Tim that this particular example is better in longhand.
[Antoine]
"Better" is an understatement :-( Now that I understood it (thanks for the explanation),
Ah, sorry - I had no idea it was the "left to right evaluation" part you weren't seeing. Next time explain why you think something is broken?
the shorthand version appears completely bonkers.
I wouldn't go that far, but I already said I wouldn't write it that way. However, without looking at real code, people are just flat-out guessing about how bad - or good - things _can_ get, no matter how confident they sound. So at least give me credit for presenting the _worst_ brief binding-expression example you've seen too ;-)
On Wed, 25 Apr 2018 18:55:56 -0500 Tim Peters <tim.peters@gmail.com> wrote:
the shorthand version appears completely bonkers.
I wouldn't go that far, but I already said I wouldn't write it that way.
However, without looking at real code, people are just flat-out guessing about how bad - or good - things _can_ get, no matter how confident they sound.
So at least give me credit for presenting the _worst_ brief binding-expression example you've seen too ;-)
I had no idea you were a bit short on them, so I'll gladly give you credits for it :-) But I hope you'll use them responsibly! Regards Antoine.
On 04/25/2018 02:55 PM, Tim Peters wrote:
This becomes a question of seasoned judgment. For example, here's a real loop summing a series expansion, until the new terms become so small they make no difference to the running total (a common enough pattern in code slinging floats or decimals):
while True: old = total total += term if old == total: return total term *= mx2 / (i*(i+1)) i += 2
To my eyes, this is genuinely harder to follow, despite its relative brevity:
while total != (total := total + term): term *= mx2 / (i*(i+1)) i += 2 return total
So I wouldn't use binding expressions in that case. I don't have a compelling head argument for _why_ I find the latter spelling harder to follow, but I don't need a theory to know that I in fact do.
I know why I do: I see "while total != total" and my gears start stripping. On the other hand, while total != (total + term as total): ... I find still intelligible. (Yes, I know "as" is dead, just wanted to throw that out there.) -- ~Ethan~
On 04/25/2018 03:15 PM, Ethan Furman wrote:
On 04/25/2018 02:55 PM, Tim Peters wrote:
This becomes a question of seasoned judgment. For example, here's a real loop summing a series expansion, until the new terms become so small they make no difference to the running total (a common enough pattern in code slinging floats or decimals):
while True: old = total total += term if old == total: return total term *= mx2 / (i*(i+1)) i += 2
To my eyes, this is genuinely harder to follow, despite its relative brevity:
while total != (total := total + term): term *= mx2 / (i*(i+1)) i += 2 return total
So I wouldn't use binding expressions in that case. I don't have a compelling head argument for _why_ I find the latter spelling harder to follow, but I don't need a theory to know that I in fact do.
I know why I do: I see "while total != total" and my gears start stripping. On the other hand,
while total != (total + term as total): ...
I find still intelligible. (Yes, I know "as" is dead, just wanted to throw that out there.)
Having said that, since whomever mentioned reading ":=" as "which is", I'm good with ":=". -- ~Ethan~
On Wed, Apr 25, 2018 at 3:15 PM, Ethan Furman <ethan@stoneleaf.us> wrote:
On 04/25/2018 02:55 PM, Tim Peters wrote:
To my eyes, this is genuinely harder to follow, despite its relative brevity:
while total != (total := total + term): term *= mx2 / (i*(i+1)) i += 2 return total
So I wouldn't use binding expressions in that case. I don't have a compelling head argument for _why_ I find the latter spelling harder to follow, but I don't need a theory to know that I in fact do.
I know why I do: I see "while total != total" and my gears start stripping. On the other hand,
while total != (total + term as total): ...
I find still intelligible. (Yes, I know "as" is dead, just wanted to throw that out there.)
The problem with either variant is that they hinge on subtle left-to-right evaluation rules. Python tries to promise left-to-right evaluation "except when it doesn't apply", e.g. in assignments the RHS is typically evaluated before subexpressions in the LHS: a[f()] = g() calls g() before f(). The example is supposed to load the left operand to != on the stack before loading the right operand, but the rule that says the left operand is evaluated before the right operand is much weaker than other evaluation order rules (like the rule stating that the arguments are evaluated before the function is called -- and before you laugh, in Algol-60 that wasn't always the case). This argument applies regardless of which syntactic form you use, and no matter what we choose, the PEP will have to clarify evaluation order in more cases than the current reference manual. (IIRC Nathaniel brought this up.) -- --Guido van Rossum (python.org/~guido)
Tim Peters wrote:
To my eyes, this is genuinely harder to follow, despite its relative brevity:
while total != (total := total + term):
Not surprising, since there are at least two deeper levels of subtlety at play: 1. total isn't just naming a subexpression, it's being rebound to something that depends on its previous value. 2. Order of evaluation is being relied on to ensure that the new value of total is compared to its old value. -- Greg
On Wed, Apr 25, 2018 at 1:55 PM, Łukasz Langa <lukasz@langa.pl> wrote:
On 25 Apr, 2018, at 1:28 PM, Guido van Rossum <guido@python.org> wrote:
You don't seem to grasp the usability improvements this will give. I hear you but at this point appeals to Python's "Zen" don't help you.
This reads dismissive to me. I did read the PEP and followed the discussion on python-dev.
It was meant dismissive. With Chris, I am tired of every core dev starting their own thread about how PEP 572 threatens readability or doesn't reach the bar for new syntax (etc.). These arguments are entirely emotional and subjective. And that's how big decisions get made. Nobody can predict the outcome with sufficient accuracy. It's like buying a new car or house. In the end you decide with your gut.
I referred to PEP 20 because it distills what's unique about the value proposition of Python. It's our shared vocabulary.
It's poetry, not a set of axioms. You can't *prove* anything with an appeal to PEP 20. You can appeal to it, for sure, but such an appeal *by definition* is subjective and emotional. (There's Only One Way To Do It? Give me a break. :-)
Can you address the specific criticism I had? To paraphrase it without PEP 20 jargon:
(name := expression) makes code less uniform. It inserts more information into a place that is already heavily packed with information (logic tests).
Most Python features make code less uniform in order to make it less repetitive. (Who needs classes? :-) -- --Guido van Rossum (python.org/~guido)
On Wed, Apr 25, 2018 at 5:58 PM Guido van Rossum <guido@python.org> wrote: [..]
It was meant dismissive. With Chris, I am tired of every core dev starting their own thread about how PEP 572 threatens readability or doesn't reach the bar for new syntax (etc.). These arguments are entirely emotional and subjective.
FWIW I started my thread for allowing '=' in expressions to make sure that we fully explore that path. I don't like ':=' and I thought that using '=' can make the idea more appealing to myself and others. It didn't, sorry if it caused any distraction. Although adding a new ':=' operator isn't my main concern. I think it's a fact that PEP 572 makes Python more complex. Teaching/learning Python will inevitably become harder, simply because there's one more concept to learn. Just yesterday this snippet was used on python-dev to show how great the new syntax is: my_func(arg, buffer=(buf := [None]*get_size()), size=len(buf)) To my eye this is an anti-pattern. One line of code was saved, but the other line becomes less readable. The fact that 'buf' can be used after that line means that it will be harder for a reader to trace the origin of the variable, as a top-level "buf = " statement would be more visible. The PEP lists this example as an improvement: [(x, y, x/y) for x in input_data if (y := f(x)) > 0] I'm an experienced Python developer and I can't read/understand this expression after one read. I have to read it 2-3 times before I trace where 'y' is set and how it's used. Yes, an expanded form would be ~4 lines long, but it would be simple to read and therefore review, maintain, and update. Assignment expressions seem to optimize the *writing code* part, while making *reading* part of the job harder for some of us. I write a lot of Python, but I read more code than I write. If the PEP gets accepted I'll use the new syntax sparingly, sure. My main concern, though, is that this PEP will likely make my job as a code maintainer harder in the end, not easier. I hope I explained my -1 on the PEP without sounding emotional. Thank you, Yury Yury
On Thu, Apr 26, 2018 at 10:11 AM, Yury Selivanov <yselivanov.ml@gmail.com> wrote:
Just yesterday this snippet was used on python-dev to show how great the new syntax is:
my_func(arg, buffer=(buf := [None]*get_size()), size=len(buf))
To my eye this is an anti-pattern. One line of code was saved, but the other line becomes less readable. The fact that 'buf' can be used after that line means that it will be harder for a reader to trace the origin of the variable, as a top-level "buf = " statement would be more visible.
Making 'buf' more visible is ONLY a virtue if it's going to be used elsewhere. Otherwise, the name 'buf' is an implementation detail of the fact that this function wants both a buffer and a size. Should you want to expand this out over more lines, you could do this: template = [None] buf = template*get_size() length = len(buf) my_func(arg, buffer=buf, size=length) What are the names 'template' and 'length' achieving? Why should they claim your attention? They are useless relics of a done-and-dusted calculation, being retained for no reason. They do not deserve top-level placement. The form as given above is starting to get a bit noisy, but I strongly disagree that 'buf' deserves to be a stand-alone name. It is as valueless as 'template' is. ChrisA
On Thu, 26 Apr 2018 10:20:40 +1000 Chris Angelico <rosuav@gmail.com> wrote:
On Thu, Apr 26, 2018 at 10:11 AM, Yury Selivanov <yselivanov.ml@gmail.com> wrote:
Just yesterday this snippet was used on python-dev to show how great the new syntax is:
my_func(arg, buffer=(buf := [None]*get_size()), size=len(buf))
To my eye this is an anti-pattern. One line of code was saved, but the other line becomes less readable. The fact that 'buf' can be used after that line means that it will be harder for a reader to trace the origin of the variable, as a top-level "buf = " statement would be more visible.
Making 'buf' more visible is ONLY a virtue if it's going to be used elsewhere. Otherwise, the name 'buf' is an implementation detail of the fact that this function wants both a buffer and a size. Should you want to expand this out over more lines, you could do this:
template = [None] buf = template*get_size() length = len(buf) my_func(arg, buffer=buf, size=length)
What are the names 'template' and 'length' achieving? Why should they claim your attention?
What is the name 'buf' in the binding expression achieving? Why should it claim my attention? It's not any different: it's just something that's used in a statement then unnecessary. Yet it will persist until the end of the enclosing scope, being retained for no reason. Perhaps we need C-like nested scopes, if such is the concern about names that live for too long? (of course, the fact that `my_func` needs you to pass its argument's length as a separate argument, while it could compute it by itself, is a bit silly) As a side note, personally, I'm usually much more concerned about the lifetime of *values* than the lifetime of names. The latter are cheap, the former can represent expensive resources. Regards Antoine.
They are useless relics of a done-and-dusted calculation, being retained for no reason. They do not deserve top-level placement.
The form as given above is starting to get a bit noisy, but I strongly disagree that 'buf' deserves to be a stand-alone name. It is as valueless as 'template' is.
ChrisA
On Wed, Apr 25, 2018 at 8:22 PM Chris Angelico <rosuav@gmail.com> wrote: [..]
my_func(arg, buffer=(buf := [None]*get_size()), size=len(buf))
To my eye this is an anti-pattern. One line of code was saved, but the other line becomes less readable. The fact that 'buf' can be used after that line means that it will be harder for a reader to trace the origin
of
the variable, as a top-level "buf = " statement would be more visible.
Making 'buf' more visible is ONLY a virtue if it's going to be used elsewhere. Otherwise, the name 'buf' is an implementation detail of the fact that this function wants both a buffer and a size. Should you want to expand this out over more lines, you could do this:
Chris, you didn't read that paragraph in my email to the end or I did a poor job at writing it. My point is that "buf" can still be used below that line, and therefore sometimes it will be used, as a result of quick refactoring or poor coding style. It's just how things happen when you write code: it gets rewritten and parts of it left outdated or not properly revised. *If* "buf" is used below that line it *will* be harder to find where it was initially set. Anyways, I don't want to distract everyone further so I'm not interested in continuing the discussion about what is readable and what is not. My own opinion on this topic is unlikely to change. I wanted to explain my -1; hopefully it will be noted. Yury
On 25 Apr, 2018, at 5:20 PM, Chris Angelico <rosuav@gmail.com> wrote:
On Thu, Apr 26, 2018 at 10:11 AM, Yury Selivanov <yselivanov.ml@gmail.com> wrote:
Just yesterday this snippet was used on python-dev to show how great the new syntax is:
my_func(arg, buffer=(buf := [None]*get_size()), size=len(buf))
To my eye this is an anti-pattern. One line of code was saved, but the other line becomes less readable. The fact that 'buf' can be used after that line means that it will be harder for a reader to trace the origin of the variable, as a top-level "buf = " statement would be more visible.
Making 'buf' more visible is ONLY a virtue if it's going to be used elsewhere. Otherwise, the name 'buf' is an implementation detail of the fact that this function wants both a buffer and a size.
You're claiming that `:=` is nicer in this situation because it's less prominent than regular assignment and thus doesn't suggest that the name stays visible later. But as others said, `:=` *does* make the name visible later until the enclosing scope ends. In fact, a large part of its appeal is that you can use the result later (as in the `re.search()` example). Will it be visible enough to the reaser in those cases then? There seems to be a conflict there. The question of assignment visibility also makes me think about unintentional name shadowing:: buf = some_value ... # 20 lines my_func(arg, buffer=(buf := [None]*get_size()), size=len(buf)) ... # 20 lines buf # <-- What value does this have? Even if we're not using the call pattern, there can be plenty of logic tests which aren't very obvious:: buf = some_value ... # 20 lines if node.parent is not None and (buf := node.parent.buffer): ... # 10 lines ... # 20 lines buf # <-- What value does this have? This is even more interesting because now `buf` isn't rebound *always*. So if I'm confused about an unexpected change in value of `buf`, I'll skim the code, fail to find the assignment, and then grep for `buf =` and also fail to find the assignment. Yes, I could have searched for just `buf` instead but that will give me too many false positives, especially if I'm using a primitive text editor search or don't know about \b in regular expressions. Debugging this can be confusing. I know it can since a similar annoyance can be observed with the magic pseudo-scope of `except`:: err = some_value try: ... except Error as err: ... err # <-- now sometimes it's not defined Just like Barry, I debugged a few cases of this in the past and within larger functions this can be hard to find. -- Ł
On Wednesday, April 25, 2018, Łukasz Langa <lukasz@langa.pl> wrote:
On 25 Apr, 2018, at 5:20 PM, Chris Angelico <rosuav@gmail.com> wrote:
On Thu, Apr 26, 2018 at 10:11 AM, Yury Selivanov <yselivanov.ml@gmail.com> wrote:
Just yesterday this snippet was used on python-dev to show how great the new syntax is:
my_func(arg, buffer=(buf := [None]*get_size()), size=len(buf))
To my eye this is an anti-pattern. One line of code was saved, but the other line becomes less readable. The fact that 'buf' can be used after that line means that it will be harder for a reader to trace the origin of the variable, as a top-level "buf = " statement would be more visible.
Making 'buf' more visible is ONLY a virtue if it's going to be used elsewhere. Otherwise, the name 'buf' is an implementation detail of the fact that this function wants both a buffer and a size.
You're claiming that `:=` is nicer in this situation because it's less prominent than regular assignment and thus doesn't suggest that the name stays visible later.
But as others said, `:=` *does* make the name visible later until the enclosing scope ends. In fact, a large part of its appeal is that you can use the result later (as in the `re.search()` example). Will it be visible enough to the reaser in those cases then?
There seems to be a conflict there.
The question of assignment visibility also makes me think about unintentional name shadowing::
buf = some_value
... # 20 lines
my_func(arg, buffer=(buf := [None]*get_size()), size=len(buf))
... # 20 lines
buf # <-- What value does this have?
Even if we're not using the call pattern, there can be plenty of logic tests which aren't very obvious::
buf = some_value
... # 20 lines
if node.parent is not None and (buf := node.parent.buffer): ... # 10 lines
... # 20 lines
buf # <-- What value does this have?
This is even more interesting because now `buf` isn't rebound *always*.
So if I'm confused about an unexpected change in value of `buf`, I'll skim the code, fail to find the assignment, and then grep for `buf =` and also fail to find the assignment. Yes, I could have searched for just `buf` instead but that will give me too many false positives, especially if I'm using a primitive text editor search or don't know about \b in regular expressions.
Debugging this can be confusing. I know it can since a similar annoyance can be observed with the magic pseudo-scope of `except`::
err = some_value try: ... except Error as err: ...
err # <-- now sometimes it's not defined
Just like Barry, I debugged a few cases of this in the past and within larger functions this can be hard to find.
Would this make it easier to put too much code on one line? Is there a good way to get *branch coverage* stats instead of just *line coverage*? Someone can probably explain with some tested pretty code for me why this would be necessary or helpful; why it wouldn't make line coverage stats more misleading for the sake of lazy?
-- Ł
On Wed, 25 Apr 2018 18:52:34 -0700 Łukasz Langa <lukasz@langa.pl> wrote:
On 25 Apr, 2018, at 5:20 PM, Chris Angelico <rosuav@gmail.com> wrote:
On Thu, Apr 26, 2018 at 10:11 AM, Yury Selivanov <yselivanov.ml@gmail.com> wrote:
Just yesterday this snippet was used on python-dev to show how great the new syntax is:
my_func(arg, buffer=(buf := [None]*get_size()), size=len(buf))
To my eye this is an anti-pattern. One line of code was saved, but the other line becomes less readable. The fact that 'buf' can be used after that line means that it will be harder for a reader to trace the origin of the variable, as a top-level "buf = " statement would be more visible.
Making 'buf' more visible is ONLY a virtue if it's going to be used elsewhere. Otherwise, the name 'buf' is an implementation detail of the fact that this function wants both a buffer and a size.
You're claiming that `:=` is nicer in this situation because it's less prominent than regular assignment and thus doesn't suggest that the name stays visible later.
But as others said, `:=` *does* make the name visible later until the enclosing scope ends. In fact, a large part of its appeal is that you can use the result later (as in the `re.search()` example). Will it be visible enough to the reaser in those cases then?
There seems to be a conflict there.
Not only, but seeing `:=` hints that something *special* is going on (some inner expression is being bound to a name). So now we have to be extra careful when reading and reviewing code written that people who like using that syntactical feature. I also wonder how long it will be before someone writes: def f(arg): global _lazy_value if predicate(arg) and (_lazy_value := frobnicate()) > arg: ... (or something similar with "nonlocal") Regards Antoine.
On Thu, Apr 26, 2018 at 10:17:34AM +0200, Antoine Pitrou wrote:
I also wonder how long it will be before someone writes:
def f(arg): global _lazy_value if predicate(arg) and (_lazy_value := frobnicate()) > arg: ...
(or something similar with "nonlocal")
What if they did? I don't think that's especially worse than any other use of a global, including the current version: def f(arg): global _lazy_value _lazy_value = frobnicate() if predicate(arg) and _lazy_value > arg: ... I'm not putting either version forward as paragons of Pythonic code, but honestly, they're not so awful that we should reject this PEP because of the risk that somebody will do this. People write crappy code and misuse features all the time. I cannot count the number of times people write list comps just for the side-effects, intentionally throwing away the resulting list: [alist.sort() for alist in list_of_lists] [print(alist) for alist in list_of_lists] and the honest truth is that when I'm engaged in exploratory coding in the REPR, sometimes if I'm feeling lazy I'll do it too. The world goes on, and comprehensions are still useful even if people sometimes misuse them. -- Steve
Łukasz Langa writes:
On 25 Apr, 2018, at 5:20 PM, Chris Angelico <rosuav@gmail.com> wrote:
You're claiming that `:=` is nicer in this situation because it's less prominent than regular assignment and thus doesn't suggest that the name stays visible later.
FWIW, I read what he wrote as "*assuming* buf is not going to be used later", and thus took a more nuanced idea from it: Use a separate statement when you do use it later, and use a binding expression when its scope is *in fact* only that line. BTW, I don't find the "it could be misused, so it will be misused" argument persuasive. I agree it's true, but don't think it matters, per the "consenting adults" principle, since binding expressions have other, important, use cases. We could add a statement to PEP 8 mildly deprecating this particular use. How about this: In some examples, the binding expression is used in function arguments where one argument depends on an earlier one, such as foo(buffer=(buf := [None]*get_size()), size=len(buf)) In examples like this one, it is preferable where possible to refactor the function to calculate the dependent variable itself, or to use an assignment statement to define the bound variable. The latter style is *strongly* preferred when the bound variable will be used later in the scope. Steve
On Thu, Apr 26, 2018 at 10:11 AM, Yury Selivanov <yselivanov.ml@gmail.com> wrote:
my_func(arg, buffer=(buf := [None]*get_size()), size=len(buf))
Obviously what we want here is a variant of the binding expression that also makes it a keyword argument: my_func(arg, buffer ::= [None]*get_size(), size = len(buffer)) -- Greg
On 4/25/2018 8:20 PM, Chris Angelico wrote:
On Thu, Apr 26, 2018 at 10:11 AM, Yury Selivanov <yselivanov.ml@gmail.com> wrote:
Just yesterday this snippet was used on python-dev to show how great the new syntax is:
my_func(arg, buffer=(buf := [None]*get_size()), size=len(buf))
What strikes me as awful about this example is that len(buf) is get_size(), so the wrong value is being named and saved. 'size=len(buf)' is, in a sense, backwards. buflen = get_size() my_func(arg, buffer = [None]*buflen, size=buflen) Is standard, clear Python code. I do not see that my_func(arg, buffer=[None]*(buflen:=get_size()), size=buflen) is an improvement. -- Terry Jan Reedy
On Thu, Apr 26, 2018 at 03:31:13AM -0400, Terry Reedy wrote:
On 4/25/2018 8:20 PM, Chris Angelico wrote:
On Thu, Apr 26, 2018 at 10:11 AM, Yury Selivanov <yselivanov.ml@gmail.com> wrote:
Just yesterday this snippet was used on python-dev to show how great the new syntax is:
my_func(arg, buffer=(buf := [None]*get_size()), size=len(buf))
What strikes me as awful about this example is that len(buf) is get_size(), so the wrong value is being named and saved. 'size=len(buf)' is, in a sense, backwards.
Terry is absolutely right, and I'm to blame for that atrocity. Mea culpa. But Yury is misrepresenting the context in which I came up with that snippet. It was not "to show how great the new syntax is", but to try to give a *more realistic example of what we might right, instead of the toy example he had given: Yuri claimed that my_func(a=(b:=foo)) was "barely readable" and I responded: 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: Alas, my spur of the moment example was crap, as you point out, but the point still stands: Yuri's example is mysterious, because there's a local variable b assigned to which doesn't seem to be used anywhere. It is either a mistake of some sort, simply poor code, or maybe b is used somewhere else in the function. Which seems poor style: if b is intended to be used elsewhere in the function, why not use normal assignment? Using a binding expression is a hint that it is likely *intended* to only be used in the current expression, or block. That's not a rule that the compiler ought to enforce, but it is a reasonable stylistic idiom, like using ALLCAPS for constants. At least, that's my opinion. So, crappy example or not, if I see a binding expression, that hints that the name used is needed in the current expression multiple times. It certainly should motivate me to look further ahead in the current expression to see where the newly defined variable is used next, and if it is only used once, to wonder if there has been some mistake. Whereas a stand alone assignment doesn't really give any hint (apart from vertical proximity, which is a very poor hint) as to how often and where a variable is used. -- Steve
On Thu, Apr 26, 2018 at 8:56 AM, Steven D'Aprano <steve@pearwood.info> wrote:
On 4/25/2018 8:20 PM, Chris Angelico wrote:
On Thu, Apr 26, 2018 at 10:11 AM, Yury Selivanov <yselivanov.ml@gmail.com> wrote:
Just yesterday this snippet was used on python-dev to show how great
On Thu, Apr 26, 2018 at 03:31:13AM -0400, Terry Reedy wrote: the
new syntax is:
my_func(arg, buffer=(buf := [None]*get_size()), size=len(buf))
What strikes me as awful about this example is that len(buf) is get_size(), so the wrong value is being named and saved. 'size=len(buf)' is, in a sense, backwards.
Terry is absolutely right, and I'm to blame for that atrocity. Mea culpa.
Perhaps a better spelling would be
my_func(arg, buffer=[None]*(buflen := get_size()), size=buflen) [...]
2018-04-26 13:20 GMT+03:00 Steve Holden <steve@holdenweb.com>:
On Thu, Apr 26, 2018 at 8:56 AM, Steven D'Aprano <steve@pearwood.info> wrote:
On 4/25/2018 8:20 PM, Chris Angelico wrote:
On Thu, Apr 26, 2018 at 10:11 AM, Yury Selivanov <yselivanov.ml@gmail.com> wrote:
Just yesterday this snippet was used on python-dev to show how great
On Thu, Apr 26, 2018 at 03:31:13AM -0400, Terry Reedy wrote: the
new syntax is:
my_func(arg, buffer=(buf := [None]*get_size()), size=len(buf))
What strikes me as awful about this example is that len(buf) is get_size(), so the wrong value is being named and saved. 'size=len(buf)' is, in a sense, backwards.
Terry is absolutely right, and I'm to blame for that atrocity. Mea culpa.
Perhaps a better spelling would be
my_func(arg, buffer=[None]*(buflen := get_size()), size=buflen)
I know it is non productive and spamy (I promise, this is the last) since `as` syntax is dead. In many cases, there is not much difference in perception between `:=` and `as`. But in several situations, like this one and as Ethan pointed up-thread - the expression first syntax makes obvious the intent and linearly readable: my_func(arg, buffer=[None]*get_size() as buf, size=buf) In any case, it is rather an anti-pattern than a good example to follow. p.s.: as Victor Stinner wrote on twitter that previously, there was a similar PEP in spirit - "PEP 379 -- Adding an Assignment Expression", which was withdrawn. May be it is worth to make a link to it in the current PEP. With kind regards, -gdg
On 4/26/2018 6:20 AM, Steve Holden wrote:
On Thu, Apr 26, 2018 at 8:56 AM, Steven D'Aprano <steve@pearwood.info <mailto:steve@pearwood.info>> wrote:
On Thu, Apr 26, 2018 at 03:31:13AM -0400, Terry Reedy wrote: > On 4/25/2018 8:20 PM, Chris Angelico wrote: > >On Thu, Apr 26, 2018 at 10:11 AM, Yury Selivanov > ><yselivanov.ml@gmail.com <mailto:yselivanov.ml@gmail.com>> wrote: > >>Just yesterday this snippet was used on python-dev to show how great the > >>new syntax is: > >> > >> my_func(arg, buffer=(buf := [None]*get_size()), size=len(buf)) > > What strikes me as awful about this example is that len(buf) is > get_size(), so the wrong value is being named and saved. > 'size=len(buf)' is, in a sense, backwards.
Terry is absolutely right, and I'm to blame for that atrocity. Mea culpa.
Perhaps a better spelling would be
my_func(arg, buffer=[None]*(buflen := get_size()), size=buflen)
That is exactly what I wrote in the continuation that Steven snipped. -- Terry Jan Reedy
On Apr 25, 2018, at 8:11 PM, Yury Selivanov <yselivanov.ml@gmail.com> wrote:
FWIW I started my thread for allowing '=' in expressions to make sure that we fully explore that path. I don't like ':=' and I thought that using '=' can make the idea more appealing to myself and others. It didn't, sorry if it caused any distraction. Although adding a new ':=' operator isn't my main concern.
I think it's a fact that PEP 572 makes Python more complex. Teaching/learning Python will inevitably become harder, simply because there's one more concept to learn.
Just yesterday this snippet was used on python-dev to show how great the new syntax is:
my_func(arg, buffer=(buf := [None]*get_size()), size=len(buf))
To my eye this is an anti-pattern. One line of code was saved, but the other line becomes less readable. The fact that 'buf' can be used after that line means that it will be harder for a reader to trace the origin of the variable, as a top-level "buf = " statement would be more visible.
The PEP lists this example as an improvement:
[(x, y, x/y) for x in input_data if (y := f(x)) > 0]
I'm an experienced Python developer and I can't read/understand this expression after one read. I have to read it 2-3 times before I trace where 'y' is set and how it's used. Yes, an expanded form would be ~4 lines long, but it would be simple to read and therefore review, maintain, and update.
Assignment expressions seem to optimize the *writing code* part, while making *reading* part of the job harder for some of us. I write a lot of Python, but I read more code than I write. If the PEP gets accepted I'll use the new syntax sparingly, sure. My main concern, though, is that this PEP will likely make my job as a code maintainer harder in the end, not easier.
I hope I explained my -1 on the PEP without sounding emotional.
FWIW, I concur with all of Yuri's thoughtful comments. After re-reading all the proposed code samples, I believe that adopting the PEP will make the language harder to teach to people who are not already software engineers. To my eyes, the examples give ample opportunity for being misunderstood and will create a need to puzzle-out the intended semantics. On the plus side, the proposal does address the occasional minor irritant of writing an assignment on a separate line. On the minus side, the visual texture of the new code is less appealing. The proposal also messes with my mental model for the distinction between expressions and statements. It probably doesn't matter at this point (minds already seem to be made up), but put me down for -1. This is a proposal we can all easily live without. Raymond
[Raymond Hettinger <raymond.hettinger@gmail.com>]
After re-reading all the proposed code samples, I believe that adopting the PEP will make the language harder to teach to people who are not already software engineers.
Can you elaborate on that? I've used dozens of languages over the decades, most of which did have some form of embedded assignment. Yes, I'm a software engineer, but I've always pitched in on "help forums" too. One language feature conspicuous by absence in newbie confusions was, consistently, assignment expressions. Read any book or tutorial for such a language, and you'll find very little space devoted to them too. What's to learn? If they understand "binding a name" _at all_ (which they must to even begin to write a non-trivial program), the only twist is that a binding expression returns the value being bound. Binding expressions certainly wouldn't be the _first_ thing to teach people. But by the time it would make sense to teach them, it's hard for me to grasp how a student could struggle with such a tiny variation on what they've already learned (all the subtleties are in what - exactly - "binding"means - which they already faced the first time they saw "j = 1").
To my eyes, the examples give ample opportunity for being misunderstood and will create a need to puzzle-out the intended semantics.
Some do, many don't. The same can be said of a great many constructs ;-)
...
[Uncle T]
One language feature conspicuous by absence in newbie confusions was, consistently, assignment expressions. Read any book or tutorial for such a language, and you'll find very little space devoted to them too.
Well, you have an entire code style built around this feature called Yoda conditions. You teach people on Day 1 to never ever confuse == with =. Some compilers even warn about this because so many people did it wrong.
What's to learn? If they understand "binding a name" _at all_ (which they must to even begin to write a non-trivial program), the only twist is that a binding expression returns the value being bound.
Ha, not in Python! Here we have *different syntax* for assignments in expressions. Well, you can also use it as a statement. But don't! We have a better one for that. And that one supports type annotations, can unpack and assign to many targets at the same time, and can even increment, multiply and so on, at once. But the other one can't. So only use the Pascal one in expressions. But don't forget parentheses, otherwise it will bind the thing you probably didn't want anyway.
To my eyes, the examples give ample opportunity for being misunderstood and will create a need to puzzle-out the intended semantics.
Some do, many don't.
As soon as we have to wrap a part of an expression in parentheses, parsing the entire thing becomes more complex. Often enough it will cause the expression to exceed whatever line length limit the codebase pledged not to exceed, causing one line to become three. And again, making it trickier for a regular Łukasz to understand what's going on. -- Ł
[Tim]
One language feature conspicuous by absence in newbie confusions was, consistently, assignment expressions. Read any book or tutorial for such a language, and you'll find very little space devoted to them too.
[Łukasz Langa <lukasz@langa.pl>]
Well, you have an entire code style built around this feature called Yoda conditions. You teach people on Day 1 to never ever confuse == with =. Some compilers even warn about this because so many people did it wrong.
Sorry, I couldn't follow that. In languages like C that use easily confused operator symbols, sure, people are forever typing "=" when they mean "==". That's nothing to do with whether they _understand_ what the different operators do, though. They do. In languages like Icon (that use "=" for numeric comparison and ":=" for assignment), that never occurs. But I'm not sure that addressed the point you were making.
What's to learn? If they understand "binding a name" _at all_ (which they must to even begin to write a non-trivial program), the only twist is that a binding expression returns the value being bound.
Ha, not in Python! Here we have *different syntax* for assignments in expressions.
Yes, binding expressions in the current PEP support an extremely limited subset of what Python's assignment statements support. That they use different operator symbols is irrelevant to that the meaning of "binding a name" is exactly the same for both.. _That's_ the "hard part" to learn.
Well, you can also use it as a statement. But don't!
Why not? _Every_ expression in Python can be used as a statement. Nothing forbids it, and that's even (very!) useful at an interactive prompt.
We have a better one for that.
As a matter of style, sure, it's best to use the simplest thing that works. As a statement in a program (as opposed to typed at a shell), "a := 3" has the unnecessary (in that context) property of returning (and discarding 3), so it's better style to use "a = 3" in that context.
And that one supports type annotations, can unpack and assign to many targets at the same time, and can even increment, multiply and so on, at once. But the other one can't.
So? math.sqrt() blows up when passed -1, but cmath.sqrt() doesn't. Different tools for different tasks.
So only use the Pascal one in expressions. But don't forget parentheses, otherwise it will bind the thing you probably didn't want anyway.
[Raymond]
To my eyes, the examples give ample opportunity for being misunderstood and will create a need to puzzle-out the intended semantics.
Some do, many don't.
As soon as we have to wrap a part of an expression in parentheses, parsing the entire thing becomes more complex. Often enough it will cause the expression to exceed whatever line length limit the codebase pledged not to exceed, causing one line to become three. And again, making it trickier for a regular Łukasz to understand what's going on.
At this point I think you must have a lower opinion of Python programmers than I have ;-) If adding even a dozen characters to a line makes it exceed a reasonable line-length guide, the code was almost certainly too confusingly dense to begin with. All the binding-expression examples I've given as "improvements" had _oceans_ of horizontal white space to swim in. Guido's if/elif/elif/elif/ ... complex text-processing example didn't, but because the current lack of an ability to bind-and-test in one gulp forced the `elif` parts to be ever-more-deeply-indented `if` blocks instead. So, to match your sarcasm, here's mine: try using a feature for what it's good at instead of for what it's bad at ;-)
Tim Peters wrote:
As a statement in a program (as opposed to typed at a shell), "a := 3" has the unnecessary (in that context) property of returning (and discarding 3), so it's better style to use "a = 3" in that context.
That seems like a post-hoc justification. If := were the one and only assignment symbol, the compiler could easily optimise away the extra DUP_TOP or whatever is involved. -- Greg
On Thu, Apr 26, 2018 at 07:16:28PM +1200, Greg Ewing wrote:
Tim Peters wrote:
As a statement in a program (as opposed to typed at a shell), "a := 3" has the unnecessary (in that context) property of returning (and discarding 3), so it's better style to use "a = 3" in that context.
That seems like a post-hoc justification. If := were the one and only assignment symbol, the compiler could easily optimise away the extra DUP_TOP or whatever is involved.
Its still bad form to rely on compiler-dependent optimizations which aren't part of the language specification. And to steal an earlier idea from Tim, it is especially unfortunate if you copy data := sorted(huge_list_with_billions_of_items) from a program and paste it into your REPL, then can't type again for an hour or two. The longer I think about this, the more I am convinced that having two forms of assignment, one a statement with no return value and the other an expression with a return value, is a feature, not a wart or bug. Yes, it does add some complexity to the language, but it is *useful* complexity. If all complexity was bad, we'd still be programming by flicking toggle switches. -- Steve
On Thu, 26 Apr 2018 17:29:17 +1000 Steven D'Aprano <steve@pearwood.info> wrote:
On Thu, Apr 26, 2018 at 07:16:28PM +1200, Greg Ewing wrote:
Tim Peters wrote:
As a statement in a program (as opposed to typed at a shell), "a := 3" has the unnecessary (in that context) property of returning (and discarding 3), so it's better style to use "a = 3" in that context.
That seems like a post-hoc justification. If := were the one and only assignment symbol, the compiler could easily optimise away the extra DUP_TOP or whatever is involved.
Its still bad form to rely on compiler-dependent optimizations which aren't part of the language specification.
If such were the need, you could very well make it part of the language specification. We are talking about a trivial optimization that any runtime could easily implement (e.g. if a sequence `DUP_TOP, STORE_FAST, POP_TOP` occurs, replace it with `STORE_FAST`). Any runtime already has to implement a set of performance properties that's far less trivial than that. For example, any decent runtime is expected to provide amortized O(1) list append or dict insertion. You are breaking user expectations if you don't.
And to steal an earlier idea from Tim, it is especially unfortunate if you copy
data := sorted(huge_list_with_billions_of_items)
from a program and paste it into your REPL, then can't type again for an hour or two.
Well, how do languages where assignment is an expression returning the assigned value make their REPLs work? I'm sure they don't inflict that on their users, so it's certainly a solvable problem. (abstractly: if the user is executing a top-level assignment expression, don't display the assignment result)
The longer I think about this, the more I am convinced that having two forms of assignment, one a statement with no return value and the other an expression with a return value, is a feature, not a wart or bug.
Yet, curiously, no other language seems to replicate that "feature" :-) It's nice to innovate, but being the only one to do something may very well mean that you're doing something ridiculous. Regards Antoine.
On Thu, Apr 26, 2018 at 6:30 PM, Antoine Pitrou <solipsis@pitrou.net> wrote:
On Thu, 26 Apr 2018 17:29:17 +1000 Steven D'Aprano <steve@pearwood.info> wrote:
On Thu, Apr 26, 2018 at 07:16:28PM +1200, Greg Ewing wrote:
Tim Peters wrote:
As a statement in a program (as opposed to typed at a shell), "a := 3" has the unnecessary (in that context) property of returning (and discarding 3), so it's better style to use "a = 3" in that context.
That seems like a post-hoc justification. If := were the one and only assignment symbol, the compiler could easily optimise away the extra DUP_TOP or whatever is involved.
Its still bad form to rely on compiler-dependent optimizations which aren't part of the language specification.
If such were the need, you could very well make it part of the language specification. We are talking about a trivial optimization that any runtime could easily implement (e.g. if a sequence `DUP_TOP, STORE_FAST, POP_TOP` occurs, replace it with `STORE_FAST`).
Not at the REPL, no. At the REPL, you need to actually print out that value. It's semantically different.
Any runtime already has to implement a set of performance properties that's far less trivial than that. For example, any decent runtime is expected to provide amortized O(1) list append or dict insertion. You are breaking user expectations if you don't.
You assume that, but it isn't always the case. Did you know, for instance, that string subscripting (an O(1) operation in CPython) is allowed to be O(n) in other Python implementations? Now you do. ChrisA
On Thu, 26 Apr 2018 19:19:05 +1000 Chris Angelico <rosuav@gmail.com> wrote:
If such were the need, you could very well make it part of the language specification. We are talking about a trivial optimization that any runtime could easily implement (e.g. if a sequence `DUP_TOP, STORE_FAST, POP_TOP` occurs, replace it with `STORE_FAST`).
Not at the REPL, no. At the REPL, you need to actually print out that value. It's semantically different.
The REPL compiles expressions in a different mode than regular modules, so that's entirely a strawman. The REPL doesn't care that it will spend a fraction of microseconds executing two additional bytecodes before presenting something to the user.
Any runtime already has to implement a set of performance properties that's far less trivial than that. For example, any decent runtime is expected to provide amortized O(1) list append or dict insertion. You are breaking user expectations if you don't.
You assume that, but it isn't always the case.
Yeah, so what?
Did you know, for instance, that string subscripting (an O(1) operation in CPython) is allowed to be O(n) in other Python implementations?
Why would I give a sh*t? Here, we are not talking about a non-trivial design decision such as how to represent string values internally (utf-8 vs. fixed-width, etc.). We are talking about optimizing a trivial bytecode sequence into another more trivial bytecode sequence. CPython can easily do it. PyPy can easily do it. Other runtimes can easily do it. If some implementation is significantly slower because it can't optimize away pointless DUP_TOPs *and* it implements DUP_TOP inefficiently enough to have user-noticeable effect, then it's either 1) a deliberate toy, a proof-of-concept not meant for serious use, or 2) a pile of crap produced by incompetent people. So there's zero reason to bother about efficiency issues here. Regards Antoine.
Antoine Pitrou wrote:
Well, how do languages where assignment is an expression returning the assigned value make their REPLs work? I'm sure they don't inflict that on their users, so it's certainly a solvable problem.
I can't think of any such language that has a REPL offhand, but here's a possible solution: x := expr # doesn't print anything (x := expr) # prints the result I.e. special-case a stand-alone assignment, but allow overriding that with parens if needed. -- Greg
On Thu, Apr 26, 2018 at 10:34:29PM +1200, Greg Ewing wrote:
Antoine Pitrou wrote:
Well, how do languages where assignment is an expression returning the assigned value make their REPLs work? I'm sure they don't inflict that on their users, so it's certainly a solvable problem.
I can't think of any such language that has a REPL offhand, but here's a possible solution:
Here's the Rhino Javascript REPL: [steve@ando ~]$ rhino Rhino 1.7 release 0.7.r2.3.el5_6 2011 05 04 js> x = (a = 99) + 1 100 Here's the standard Ruby REPL: [steve@ando ~]$ irb irb(main):001:0> x = (a = 99) + 1 => 100 So both of these REPLs do print the result of the expression. R, on the other hand, doesn't print the results of assignment expressions:
x <- (a <- 99) + 1 c(x, a) [1] 100 99
Rhino, however, does suppress printing if you prefix the variable with "var" or "const". -- Steve
On 4/26/2018 6:34 AM, Greg Ewing wrote:
Antoine Pitrou wrote:
Well, how do languages where assignment is an expression returning the assigned value make their REPLs work? I'm sure they don't inflict that on their users, so it's certainly a solvable problem.
I can't think of any such language that has a REPL offhand, but here's a possible solution:
x := expr # doesn't print anything
Ugh! The only reason I would bother typing the : in a top level assignment would be to get the print without having to retype the name, as in
x = expr x
I consider echoing top-level interactive assignments to be a feature of the proposal. -- Terry Jan Reedy
FWIW, the combination of limiting the PEP to binding expressions and the motivating example of sequential if/elif tests that each need to utilize an expression in their body (e.g. matching various regexen by narrowing, avoiding repeated indent) gets me to +1. I still think the edge case changes to comprehension semantics is needless for this PEP. However, it concerns a situation I don't think I've ever encountered in the wild, and certainly never relied on the old admittedly odd behavior. On Thu, Apr 26, 2018, 2:01 AM Tim Peters <tim.peters@gmail.com> wrote:
Yes, binding expressions in the current PEP support an extremely limited subset of what Python's assignment statements support.[...] Guido's if/elif/elif/elif/ ... complex text-processing example didn't, but because the current lack of an ability to bind-and-test in one gulp forced the `elif` parts to be ever-more-deeply-indented `if` blocks instead.
[Uncle T]
So, to match your sarcasm, here's mine: try using a feature for what it's good at instead of for what it's bad at ;-)
Yes, this is the fundamental wisdom. Judging which is which is left as an exercise to the programmer. With this, I'm leaving the discussion. With Guido and you on board for PEP 572, I feel that Chris' streak is indeed about to break. Some parting hair-splitting follows.
[Uncle T]
One language feature conspicuous by absence in newbie confusions was, consistently, assignment expressions. Read any book or tutorial for such a language, and you'll find very little space devoted to them too.
[Łukasz Langa <lukasz@langa.pl>]
Well, you have an entire code style built around this feature called Yoda conditions. You teach people on Day 1 to never ever confuse == with =. Some compilers even warn about this because so many people did it wrong.
[Uncle T]
Sorry, I couldn't follow that.
You implied that newbies don't have to even know about assignments in expressions. I wanted to demonstrate that this isn't really the case because mistaking `=` for `==` is a relatively common occurence for newbies. If you want to argue that it isn't, I'd like to point out that the WordPress code style *requires* Yoda conditions because it was enough of a hindrance. ESLint (a JavaScript linter) also has a warning about assignment in a conditional. [Uncle T]]
In languages like C that use easily confused operator symbols, sure, people are forever typing "=" when they mean "==". That's nothing to do with whether they _understand_ what the different operators do, though.
What you're saying is true. But for it to be true, newbies *have to* learn the distinction, and the fact that yes, sometimes the programmer indeed meant to put a single `=` sign in the conditional. That's why we'll end up with the Pascal assignment operator. And that *is* a thing that you will have to explain to newbies when they encounter it for the first time. Sadly, googling for a colon followed by an equal sign isn't trivial if you don't know what you're looking for. [Łukasz]
Well, you can also use it as a statement. But don't!
[Uncle T]]
Why not? _Every_ expression in Python can be used as a statement. Nothing forbids it, and that's even (very!) useful at an interactive prompt.
Because it suggests different intent, because it's limited, because it's slower at runtime, and because PEP 572 says so itself.
At this point I think you must have a lower opinion of Python programmers than I have ;-) If adding even a dozen characters to a line makes it exceed a reasonable line-length guide, the code was almost certainly too confusingly dense to begin with.
Around 5% of if and elif statements in the standard library don't fit a single line *as is*. Sure, that's a low percentage but that's over 1,000 statements. If you're putting an `if` statement in a method, you are already starting out with 71 characters left on the line. Four of those are going to be taken by "if", a space, and the colon. Adding a parenthesized assignment expression takes at least 10% of that available space. The silver lining for me is that this makes the environment riper for auto-formatting. -- Ł
[Tim]
So, to match your sarcasm, here's mine: try using a feature for what it's good at instead of for what it's bad at ;-)
[Lukasz Langa <lukasz@langa.pl>]
Yes, this is the fundamental wisdom. Judging which is which is left as an exercise to the programmer.
With this, I'm leaving the discussion. With Guido and you on board for PEP 572, I feel that Chris' streak is indeed about to break.
I still expect it could go either way, but do wish people didn't believe it will be a major loss if "the other side wins". I'll be fine regardless - and so will everyone else. Guido rarely makes language design mistakes. In this case he's seeing serious opposition from several core developers, and you shouldn't believe either that he just dismisses that. [Łukasz Langa]
Well, you have an entire code style built around this feature called Yoda conditions. You teach people on Day 1 to never ever confuse == with =. Some compilers even warn about this because so many people did it wrong.
Sorry, I couldn't follow that.
Part of the problem here is that I had never seen "Yoda conditions" before, and had no idea what it meant. Some later Googling suggests it's "a thing" youngsters say at times ;-)
You implied that newbies don't have to even know about assignments in expressions. I wanted to demonstrate that this isn't really the case because mistaking `=` for `==` is a relatively common occurence for newbies. If you want to argue that it isn't, I'd like to point out that the WordPress code style *requires* Yoda conditions because it was enough of a hindrance. ESLint (a JavaScript linter) also has a warning about assignment in a conditional.
What does that have to do with Python? If they try to use "=" in an expression now, they get a SyntaxError. The PEP doesn't change anything about that. Indeed, that's why it uses ":=" instead. I have experience in other languages with embedded assignments that also use ":=", and it's _never_ the case that people type ":=" when they intend "equality test" in those. The horrid "I typed = when I meant ==" mistakes are unique to languages that mindlessly copied C. The mistakes aren't primarily due to embedded assignments, they're due to that even highly experienced programmers sometimes type "=" when they're _thinking_ "equals". Nobody types ":=" when they're thinking "equals".
... What you're saying is true. But for it to be true, newbies *have to* learn the distinction, and the fact that yes, sometimes the programmer indeed meant to put a single `=` sign in the conditional.
Again, the PEP is about Python: a single "=" in a conditional is, and will remain, a SyntaxError. So nobody can sanely intend to put a single "=" in a condition _in Python_ unless they're writing a test intending to provoke a syntax error.
That's why we'll end up with the Pascal assignment operator.
":=" is already in the PEP.
And that *is* a thing that you will have to explain to newbies when they encounter it for the first time.
Sure. That doesn't frighten me, though. It's easy to explain what it does - although it may be hard to explain when it's _desirable_ to use it.
Sadly, googling for a colon followed by an equal sign isn't trivial if you don't know what you're looking for.
To judge from Stackoverflow volume, the single most misunderstood of all Python operators - by far - is "is" - try Googling for that ;-) In far second and third places are "and" and "or", for which searches are also useless. Regardless, I'm not concerned about one-time tiny learning curves. Don't know what ":=" means already? Ask someone. If you know what "=" means, you're already close to done. Given that you already understand what "binding a name" means, ":=" may well be the simplest of all Python's operators (there's no computation _to_ be understood, and no possibility either of a dunder method changing its meaning depending on operand type(s)).
Well, you can also use it as a statement. But don't!
Why not? _Every_ expression in Python can be used as a statement. Nothing forbids it, and that's even (very!) useful at an interactive prompt.
Because it suggests different intent, because it's limited, because it's slower at runtime, and because PEP 572 says so itself.
I didn't say you're _required_ to use it as a statement. Regardless of what PEPs say, people will do what they find most useful. I trust people to figure this out quickly for themselves.
At this point I think you must have a lower opinion of Python programmers than I have ;-) If adding even a dozen characters to a line makes it exceed a reasonable line-length guide, the code was almost certainly too confusingly dense to begin with.
Around 5% of if and elif statements in the standard library don't fit a single line *as is*. Sure, that's a low percentage but that's over 1,000 statements. If you're putting an `if` statement in a method, you are already starting out with 71 characters left on the line. Four of those are going to be taken by "if", a space, and the colon. Adding a parenthesized assignment expression takes at least 10% of that available space.
Confirming that you do have a lower opinion of them ;-) Are you picturing people stampeding to introduce ":=" in every place they possibly could? I may be wrong, but I don't expect that at all. I expect a vast majority of uses in real life will replace: name = expression if name: by if name := expression: and while True: name = expression if name comparison expression2: break by while (name := expression) inverted_comparison expression2: _provided that_ the latter spelling doesn't make the line uncomfortably long. In all the code of mine I've seen a good use for it, there's a whole lot of empty horizontal screen space to spare, even after recoding. In places where I already had "long lines", I didn't even check to see whether a binding operation could be used too - why would I? I don't feel _compelled_ to use it - I'm only looking to reduce redundancy where it's an obvious win.
The silver lining for me is that this makes the environment riper for auto-formatting.
See? It's win-win for you too no matter how this turns out ;-)
On Thu, Apr 26, 2018 at 1:33 PM, Tim Peters <tim.peters@gmail.com> wrote:
And that *is* a thing that you will have to explain to newbies when they encounter
it for the first time.
Sure. That doesn't frighten me, though. It's easy to explain what it does - although it may be hard to explain when it's _desirable_ to use it.
I'm with Raymond here -- though I'm not sure "newbies" is quite right -- I've found that newbies fall into two camps: folks to whom programming comes naturally, and those that it doesn't (OK, it's a distribution, but a bimodal one). And folks that are struggling with programming can struggle even with simple assignment (name binding), particularly when you add even function local scope. So having one more way to do assignment WILL make it harder to teach, not because it's that hard, but because it's one more thing to learn. But the fact is that as Python has evolved (particularly with the jump to py3) it has become less and less of a "scripting" language, and more of a "systems" language. And also harder to learn. Anyone remember CP4E? Python is not as good choice as a "newbie" language as it once was. Adding := will move it a little bit more along the complexity path -- not much, and that's where Python has gone anyway, so as Tim said, no one's going to suffer either way this decision goes. Hmm -- I wonder if a "pythonscript" will get forked off one day...... To judge from Stackoverflow volume, the single most misunderstood of
all Python operators - by far - is "is" -
You now, I think instructors like me are partly responsible. "is" is rarely useful outside of comparing to singletons. Yet I use it early in instruction to do checks on name binding and show things with mutablilty, etc.... which has the unfortunate side effect of making it seem like a more common operator than it is. I've even had students write code like: if x is 3: and thanks to interning, it appears to work! -CHB -- Christopher Barker, Ph.D. Oceanographer Emergency Response Division NOAA/NOS/OR&R (206) 526-6959 voice 7600 Sand Point Way NE (206) 526-6329 fax Seattle, WA 98115 (206) 526-6317 main reception Chris.Barker@noaa.gov
[Lukasz]
And that *is* a thing that you will have to explain to newbies when they encounter it for the first time.
[Tim]
Sure. That doesn't frighten me, though. It's easy to explain what it does - although it may be hard to explain when it's _desirable_ to use it.
[Chris Barker <chris.barker@noaa.gov>]
I'm with Raymond here -- though I'm not sure "newbies" is quite right -- I've found that newbies fall into two camps: folks to whom programming comes naturally, and those that it doesn't (OK, it's a distribution, but a bimodal one). And folks that are struggling with programming can struggle even with simple assignment (name binding), particularly when you add even function local scope.
Sure. What I wrote was shorthand for what's already been covered at length many times: what a binding expression does is "easy to explain" GIVEN THAT someone ALREADY UNDERSTANDS how binding a name works. The latter in fact seems difficult for a significant number of people to learn, but it's utterly unavoidable that they learn it if they're ever to write non-trivial Python programs. That's been true since Python's first release. Binding expressions would be introduced much later in any sane course. At THAT point, for students who haven't already dropped out, the semantics are darned-near trivial to explain: it binds the name to the object the expression evaluates to (all of which they _already_ understand by this point), and the value of the binding expression is that object (the only new bit). Unlike as for most other operators, you don't even have to weasel-word it to account for that a magical dunder method may change what ":=" does. As for the "is" operator, the meaning is baked into the language and can't be altered in the slightest.
So having one more way to do assignment WILL make it harder to teach, not because it's that hard, but because it's one more thing to learn.
On a scale of 1 to a million, try to quantify how much harder ;-) As above, I can't see it getting beyond a single digit, GIVEN THAT a student has already masteredf the far more complex assignment _statement_ (binding expressions are limited to the single simplest case of the many things an assignment statement can do). "And it returns the object" is a yawn. But, as I already granted, it may be truly hard to explain when it's a desirable thing to use. That takes experience and "good judgment", which - according to me - can be learned but can't really be taught.
But the fact is that as Python has evolved (particularly with the jump to py3) it has become less and less of a "scripting" language, and more of a "systems" language. And also harder to learn. Anyone remember CP4E? Python is not as good choice as a "newbie" language as it once was.
I agree - although I expect sticking to a subset of Python could make life easier for beginners. For example, would anyone in their right mind even mention async gimmicks when teaching beginners? Against that, though, one of the most unintentionally funny tech things I ever read was Bjarne Stroustrup writing about why C++ is an excellent choice for beginners. But he does have a point: if you throw away the bulk of everything C++ added, there's an easily usable little language exceedingly well hidden under it all ;-)
Adding := will move it a little bit more along the complexity path -- not much, and that's where Python has gone anyway, so as Tim said, no one's going to suffer either way this decision goes.
Yet there will be much wailing and gnashing of teeth anyway ;-) ...
To judge from Stackoverflow volume, the single most misunderstood of all Python operators - by far - is "is" -
You now, I think instructors like me are partly responsible. "is" is rarely useful outside of comparing to singletons. Yet I use it early in instruction to do checks on name binding and show things with mutablilty, etc.... which has the unfortunate side effect of making it seem like a more common operator than it is.
I've even had students write code like:
if x is 3:
and thanks to interning, it appears to work!
Yup, that's the real problem with "is": its semantics are dead simple, but "but under exactly what conditions are `x` and `y` bound to the same object?" is intractable. It seems to take a long time to get across the point, that the question itself is misguided. A full answer requires delving into transient implementation details, which is counterproductive because they _are_ accidents of the implementation du jour. What questioners need to be nudged into asking instead is for examples of when using "is" is thoroughly sane.
On Fri, Apr 27, 2018 at 8:19 PM, Tim Peters <tim.peters@gmail.com> wrote:
[Lukasz]
And that *is* a thing that you will have to explain to newbies when they encounter it for the first time.
Which they will presumably do either in class or by reading code. No sensible instructor or course author is going to bring name-binding expressions up until standard assignment has been thoroughly assimilated. In my own teaching experience I observed that those used to static languages took a little time to adapt to the indirection of Python's names, but not long.
[...]
Sure. What I wrote was shorthand for what's already been covered at length many times: what a binding expression does is "easy to explain" GIVEN THAT someone ALREADY UNDERSTANDS how binding a name works. The latter in fact seems difficult for a significant number of people to learn, but it's utterly unavoidable that they learn it if they're ever to write non-trivial Python programs. That's been true since Python's first release.
I was half-expecting someone to pop up and suggest only functional programming as a means to avoid having to teach assignment ...
Binding expressions would be introduced much later in any sane course. At THAT point, for students who haven't already dropped out, the semantics are darned-near trivial to explain: it binds the name to the object the expression evaluates to (all of which they _already_ understand by this point), and the value of the binding expression is that object (the only new bit).
Unlike as for most other operators, you don't even have to weasel-word it to account for that a magical dunder method may change what ":=" does. As for the "is" operator, the meaning is baked into the language and can't be altered in the slightest.
So having one more way to do assignment WILL make it harder to teach, not because it's that hard, but because it's one more thing to learn.
But surely that depends on HOW MUCH of the language you aim to teach. Over the years Python has become a much more complex language, but it has a fairly easily-identified subset that can act as a basis for building useful programs. Some instructors avoided teaching comprehensions, but a sensible course would try to ensure that students could understand the code they were most likely to encounter "in the wild."
[
...]
You now, I think instructors like me are partly responsible. "is" is rarely
useful outside of comparing to singletons. Yet I use it early in instruction to do checks on name binding and show things with mutablilty, etc.... which has the unfortunate side effect of making it seem like a more common operator than it is.
I'd expand that to say that identity comparison is most useful for types whose instances are all unique. For other types there's the unfortunate impedance mismatch between identity and equality (which is user-definable anyway).
I've even had students write code like:
if x is 3:
and thanks to interning, it appears to work!
No, thanks to interning it DOES work. For interned values.
But instructors have to touch on implementation artefacts at times, and I used to find it instructive to write the same code with two different integer constants and ask why they gave different results. It certainly helped people to master the semantics of assignment (as did the phrase "Python never copies data on assignment"). Yup, that's the real problem with "is": its semantics are dead
simple, but "but under exactly what conditions are `x` and `y` bound to the same object?" is intractable. It seems to take a long time to get across the point, that the question itself is misguided. A full answer requires delving into transient implementation details, which is counterproductive because they _are_ accidents of the implementation du jour. What questioners need to be nudged into asking instead is for examples of when using "is" is thoroughly sane.
I'd argue that without some knowledge of the potential pitfalls students can't be expected to learn how to make that distinction. regards Steve
On Wed, Apr 25, 2018 at 10:14:11PM -0700, Łukasz Langa wrote:
So only use the Pascal one in expressions. But don't forget parentheses, otherwise it will bind the thing you probably didn't want anyway.
Binding expressions are no worse than any other expression: sometimes you need to bracket terms to change the default precedence, and sometimes you don't. And sometimes, even if we don't *need* parens, we use them anyway because it makes the expression easier to read and understand. Unless you have a language with no operator precedence at all, a purely left-to-right evaluation order like Forth or (I think?) APL, there will always be circumstances where parens are needed. Criticising binding- expressions for that reason, especially implying that we must always use parens, is simply FUD. [...]
As soon as we have to wrap a part of an expression in parentheses, parsing the entire thing becomes more complex.
Unless it becomes less complex to read and understand. I for one always have difficulty parsing complex boolean tests unless I bracket some or all of the parts, even when they're not strictly needed. Consequently I try very hard not to write complex bool tests in the first place, but when I can't avoid it, a few extra brackets really helps simplify the logic.
Often enough it will cause the expression to exceed whatever line length limit the codebase pledged not to exceed, causing one line to become three.
Just how often are your lines within two characters of the maximum column so that adding a pair of brackets () will "often enough" put it over the limit? Seems pretty unlikely to me. This sounds really like picking at straws. -- Steve
On Apr 25, 2018, at 11:10 PM, Steven D'Aprano <steve@pearwood.info> wrote: Criticising binding- expressions for that reason, especially implying that we must always use parens, is simply FUD.
The PEP has more examples with parentheses than without.
As soon as we have to wrap a part of an expression in parentheses, parsing the entire thing becomes more complex.
Unless it becomes less complex to read and understand.
You're ignoring the context of the discussion. The new parentheses are there because there's a new assignment there. That's more complex.
Often enough it will cause the expression to exceed whatever line length limit the codebase pledged not to exceed, causing one line to become three.
Just how often are your lines within two characters of the maximum column so that adding a pair of brackets () will "often enough" put it over the limit? Seems pretty unlikely to me.
Again, you're ignoring the context of the discussion. Assuming := will have spaces around it in PEP 8, this is adding *at least* 7 characters if the name of your choice is a single character.
This sounds really like picking at straws.
The entire point of the PEP is to make things "nicer". It doesn't fundamentally enable programmers to do anything they couldn't do before. If you think demonstrating cases where the end result won't be an improvement is picking at straws, then maybe the improvement of PEP 572 is as well. -- Ł
On Thu, Apr 26, 2018 at 08:48:12AM -0700, Łukasz Langa wrote:
On Apr 25, 2018, at 11:10 PM, Steven D'Aprano <steve@pearwood.info> wrote: Criticising binding- expressions for that reason, especially implying that we must always use parens, is simply FUD.
The PEP has more examples with parentheses than without.
Yes? Parens aren't mandatory, and my point that other operators also sometimes needs parens still holds.
As soon as we have to wrap a part of an expression in parentheses, parsing the entire thing becomes more complex.
Unless it becomes less complex to read and understand.
You're ignoring the context of the discussion. The new parentheses are there because there's a new assignment there. That's more complex.
I'm not ignoring the context of the discussion. I'm comparing binding- expression with and without parens. That's what I thought you were doing. If that wasn't your intended meaning, then I apologise but please understand why I answered the way I did. I still stand by my argument: parens are not always needed, and even when they are not needed, adding them can sometimes make things *easier* and *less complex* to read. [...]
If you think demonstrating cases where the end result won't be an improvement is picking at straws, then maybe the improvement of PEP 572 is as well.
Any feature can have cases where the end result is worse than not using the feature. That *alone* isn't a strong argument against a feature. Do you have much existing code using binding expressions? Of course not. Will you be adding them to code that already exists? Probably not -- you can't do so until you are using 3.8 at minimum, and if your code needs to be backwards compatible, you can't use it until you've dropped support for 3.7 and older. That might not be for five or ten years. So it is likely that for most people only new code will use this feature. It is not reasonable to say that if I have existing code like this: spam = expression if long_condition_that_takes_up_most_of_the_line == spam or spam: ... that I'm going to immediately change it to a one-liner: if long_condition_that_takes_up_most_of_the_line == (spam := expression) or spam: ... and push it over the maximum line width. With or without parentheses. Why would I do something so silly? Using binding expressions isn't mandatory and most coders don't intentionally do things that make their code worse. And if I wouldn't change existing code and push it over the limit, why would I write new code that does it? Especially when there are much better alternatives: if (long_condition_that_takes_up_most_of_the_line == (spam:=expression) or spam): ... We have a history of adding features that can be abused, but aren't. People hardly ever abuse list comps with overly long and complex multiple-loop comprehensions: [... for a in sequence for b in something for c in another for d in something_else] I'm sure we've all seen one or two of those. But it doesn't happen enough to matter. Same with if...elif...else chains. People didn't immediately run out and replace every single if chain into nested ternary if expressions, pushing their lines out to beyond the maximum line width: expression if condition else (expression if condition else (expression if condition else (expression if condition else expression))) Real people don't abuse comprehensions or ternary if enough for us to regret adding them to the language. I'm sure that they won't abuse this feature either. The Python community simply doesn't have the culture of abusing syntax in this way and writing overly dense one-liners, and I don't think it is reasonable to say this feature will tip the balance. It is reasonable to say that *some* code will be made worse by this, because there's always *someone* who will abuse syntax. There are those who insist on writing list comprehensions for their side-effects: # return and throw away a list of Nones [print(item) for item in bunch_of_stuff] but I don't think this happens enough to make us regret adding comprehensions to the language. -- Steve
So, the style guidelines for this new feature -- and also ternary expressions and comprehension -- would need to mention that: - debuggers have no idea what to do with all of this on one line - left-to-right doesn't apply to comprehensions results = [(x, y, x/y) for x in input_data if (y := f(x)) > 0] - left-to-right doesn't apply to ternary expressions if (y := func(x)) if (x := 3) else 0: while (y := func(x)) if (x := 3) else 0: - left-to-right does apply to everything else? - *these* are discouraged: if (x := 3) or (y := func(x)): if (3) or (func(3)): if ((x := 3) if 1 else (y := func(x))): IDK, I could just be resistant to change, but this seems like something that will decrease readability -- and slow down code review -- without any real performance gain. So, while this would be useful for golfed-down (!) one-liners with pyline, I'm -1 on PEP 572. How do I step through this simple example with a debugger? if re.search(pat, text) as match: print("Found:", match.group(0)) How do I explain what ':=' is when teaching Python? AFAIU, semantically: Python = ('equals') indicates a statement. What you are proposing is adding an ':=' ('colon equals') assignment operator which defines a variable which is limited in scope only in list, dict, and generator comprehensions.
From https://en.wikipedia.org/wiki/Assignment_(computer_science) :
In some languages the symbol used is regarded as an operator (meaning that the assignment has a value) while others define the assignment as a statement (meaning that it cannot be used in an expression).
PEP 572 -- Assignment Expressions PEP 572 -- Assignment Operator (:=) and Assignment Expressions On Friday, April 27, 2018, Steven D'Aprano <steve@pearwood.info> wrote:
On Thu, Apr 26, 2018 at 08:48:12AM -0700, Łukasz Langa wrote:
On Apr 25, 2018, at 11:10 PM, Steven D'Aprano <steve@pearwood.info>
wrote:
Criticising binding- expressions for that reason, especially implying that we must always use parens, is simply FUD.
The PEP has more examples with parentheses than without.
Yes? Parens aren't mandatory, and my point that other operators also sometimes needs parens still holds.
As soon as we have to wrap a part of an expression in parentheses, parsing the entire thing becomes more complex.
Unless it becomes less complex to read and understand.
You're ignoring the context of the discussion. The new parentheses are there because there's a new assignment there. That's more complex.
I'm not ignoring the context of the discussion. I'm comparing binding- expression with and without parens. That's what I thought you were doing.
If that wasn't your intended meaning, then I apologise but please understand why I answered the way I did.
I still stand by my argument: parens are not always needed, and even when they are not needed, adding them can sometimes make things *easier* and *less complex* to read.
[...]
If you think demonstrating cases where the end result won't be an improvement is picking at straws, then maybe the improvement of PEP 572 is as well.
Any feature can have cases where the end result is worse than not using the feature. That *alone* isn't a strong argument against a feature.
Do you have much existing code using binding expressions? Of course not. Will you be adding them to code that already exists? Probably not -- you can't do so until you are using 3.8 at minimum, and if your code needs to be backwards compatible, you can't use it until you've dropped support for 3.7 and older. That might not be for five or ten years.
So it is likely that for most people only new code will use this feature. It is not reasonable to say that if I have existing code like this:
spam = expression if long_condition_that_takes_up_most_of_the_line == spam or spam: ...
that I'm going to immediately change it to a one-liner:
if long_condition_that_takes_up_most_of_the_line == (spam := expression) or spam: ...
and push it over the maximum line width. With or without parentheses. Why would I do something so silly? Using binding expressions isn't mandatory and most coders don't intentionally do things that make their code worse.
And if I wouldn't change existing code and push it over the limit, why would I write new code that does it? Especially when there are much better alternatives:
if (long_condition_that_takes_up_most_of_the_line == (spam:=expression) or spam): ...
We have a history of adding features that can be abused, but aren't. People hardly ever abuse list comps with overly long and complex multiple-loop comprehensions:
[... for a in sequence for b in something for c in another for d in something_else]
I'm sure we've all seen one or two of those. But it doesn't happen enough to matter. Same with if...elif...else chains. People didn't immediately run out and replace every single if chain into nested ternary if expressions, pushing their lines out to beyond the maximum line width:
expression if condition else (expression if condition else (expression if condition else (expression if condition else expression)))
Real people don't abuse comprehensions or ternary if enough for us to regret adding them to the language. I'm sure that they won't abuse this feature either. The Python community simply doesn't have the culture of abusing syntax in this way and writing overly dense one-liners, and I don't think it is reasonable to say this feature will tip the balance.
It is reasonable to say that *some* code will be made worse by this, because there's always *someone* who will abuse syntax. There are those who insist on writing list comprehensions for their side-effects:
# return and throw away a list of Nones [print(item) for item in bunch_of_stuff]
but I don't think this happens enough to make us regret adding comprehensions to the language.
-- Steve _______________________________________________ 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/ wes.turner%40gmail.com
On Fri, Apr 27, 2018 at 8:18 PM, Wes Turner <wes.turner@gmail.com> wrote:
IDK, I could just be resistant to change, but this seems like something that will decrease readability -- and slow down code review -- without any real performance gain. So, while this would be useful for golfed-down (!) one-liners with pyline, I'm -1 on PEP 572.
PEP 572 has never promised a performance gain, so "without any real performance gain" is hardly a criticism.
How do I step through this simple example with a debugger?
if re.search(pat, text) as match: print("Found:", match.group(0))
Step the 'if' statement. It will call re.search() and stash the result in 'match'. Then the cursor would be put either on the 'print' (if the RE matched) or on the next executable line (if it didn't).
From https://en.wikipedia.org/wiki/Assignment_(computer_science) :
In some languages the symbol used is regarded as an operator (meaning that the assignment has a value) while others define the assignment as a statement (meaning that it cannot be used in an expression).
PEP 572 -- Assignment Expressions PEP 572 -- Assignment Operator (:=) and Assignment Expressions
Huh? I don't get your point. ChrisA
On Friday, April 27, 2018, Chris Angelico <rosuav@gmail.com> wrote:
IDK, I could just be resistant to change, but this seems like something
On Fri, Apr 27, 2018 at 8:18 PM, Wes Turner <wes.turner@gmail.com> wrote: that
will decrease readability -- and slow down code review -- without any real performance gain. So, while this would be useful for golfed-down (!) one-liners with pyline, I'm -1 on PEP 572.
PEP 572 has never promised a performance gain, so "without any real performance gain" is hardly a criticism.
How do I step through this simple example with a debugger?
if re.search(pat, text) as match: print("Found:", match.group(0))
Step the 'if' statement. It will call re.search() and stash the result in 'match'. Then the cursor would be put either on the 'print' (if the RE matched) or on the next executable line (if it didn't).
Right. Pdb doesn't step through the AST branches of a line, so ternary expressions and list comprehensions and defining variables at the end of the line are 'debugged' after they're done. Similarly, code coverage is line-based; so those expressions may appear to be covered but are not.
From https://en.wikipedia.org/wiki/Assignment_(computer_science) :
In some languages the symbol used is regarded as an operator (meaning that the assignment has a value) while others define the assignment as a statement (meaning that it cannot be used in an expression).
PEP 572 -- Assignment Expressions PEP 572 -- Assignment Operator (:=) and Assignment Expressions
Huh? I don't get your point.
Q: What is ':='? (How do I searchengine for it?) A: That's the assignment operator which only works in Python 3.8+. Q: When are variables defined -- or mutable names bound -- at the end of the expression accessible to the left of where they're defined? Q: What about tuple unpacking? Is there an ECMAscript-like destructuring PEP yet? A: Ternary expressions; list, dict, generator comprehensions; (@DOCS PLEASE HELP EXPLAIN THIS) Q: do these examples of the assignment expression operator all work? """ - debuggers have no idea what to do with all of this on one line - left-to-right doesn't apply to comprehensions results = [(x, y, x/y) for x in input_data if (y := f(x)) > 0] - left-to-right doesn't apply to ternary expressions if (y := func(x)) if (x := 3) else 0: while (y := func(x)) if (x := 3) else 0: - left-to-right does apply to everything else? - *these* are discouraged: if (x := 3) or (y := func(x)): if (3) or (func(3)): if ((x := 3) if 1 else (y := func(x))): """
ChrisA _______________________________________________ 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/ wes.turner%40gmail.com
On Sat, Apr 28, 2018 at 6:06 AM, Wes Turner <wes.turner@gmail.com> wrote:
On Friday, April 27, 2018, Chris Angelico <rosuav@gmail.com> wrote:
On Fri, Apr 27, 2018 at 8:18 PM, Wes Turner <wes.turner@gmail.com> wrote:
IDK, I could just be resistant to change, but this seems like something that will decrease readability -- and slow down code review -- without any real performance gain. So, while this would be useful for golfed-down (!) one-liners with pyline, I'm -1 on PEP 572.
PEP 572 has never promised a performance gain, so "without any real performance gain" is hardly a criticism.
How do I step through this simple example with a debugger?
if re.search(pat, text) as match: print("Found:", match.group(0))
Step the 'if' statement. It will call re.search() and stash the result in 'match'. Then the cursor would be put either on the 'print' (if the RE matched) or on the next executable line (if it didn't).
Right. Pdb doesn't step through the AST branches of a line, so ternary expressions and list comprehensions and defining variables at the end of the line are 'debugged' after they're done.
Similarly, code coverage is line-based; so those expressions may appear to be covered but are not.
I'm not sure I follow. In what situation would some code appear to be covered when it isn't, due to an assignment expression? ChrisA
On Friday, April 27, 2018, Chris Angelico <rosuav@gmail.com> wrote:
On Sat, Apr 28, 2018 at 6:06 AM, Wes Turner <wes.turner@gmail.com> wrote:
On Friday, April 27, 2018, Chris Angelico <rosuav@gmail.com> wrote:
On Fri, Apr 27, 2018 at 8:18 PM, Wes Turner <wes.turner@gmail.com>
IDK, I could just be resistant to change, but this seems like something that will decrease readability -- and slow down code review -- without any real performance gain. So, while this would be useful for golfed-down (!) one-liners with pyline, I'm -1 on PEP 572.
PEP 572 has never promised a performance gain, so "without any real performance gain" is hardly a criticism.
How do I step through this simple example with a debugger?
if re.search(pat, text) as match: print("Found:", match.group(0))
Step the 'if' statement. It will call re.search() and stash the result in 'match'. Then the cursor would be put either on the 'print' (if the RE matched) or on the next executable line (if it didn't).
Right. Pdb doesn't step through the AST branches of a line, so ternary expressions and list comprehensions and defining variables at the end of
wrote: the
line are 'debugged' after they're done.
Similarly, code coverage is line-based; so those expressions may appear to be covered but are not.
I'm not sure I follow. In what situation would some code appear to be covered when it isn't, due to an assignment expression?
When an assignment expression is in the else clause of a ternary expression, but the else clause does not execute because the condition is true, the assignment expression does not execute and so isn't covered. if ((1) or (x := 3)): if ((y := func(x)) if x else (x := 3)) Is this a new opportunity for misunderstanding? Assignment expressions, though they are noticeable :=, may not actually define the variable in cases where that part of the line doesn't run but reads as covered.
ChrisA _______________________________________________ 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/ wes.turner%40gmail.com
On Sat, Apr 28, 2018 at 6:24 AM, Wes Turner <wes.turner@gmail.com> wrote:
On Friday, April 27, 2018, Chris Angelico <rosuav@gmail.com> wrote:
On Sat, Apr 28, 2018 at 6:06 AM, Wes Turner <wes.turner@gmail.com> wrote:
On Friday, April 27, 2018, Chris Angelico <rosuav@gmail.com> wrote:
On Fri, Apr 27, 2018 at 8:18 PM, Wes Turner <wes.turner@gmail.com> wrote:
IDK, I could just be resistant to change, but this seems like something that will decrease readability -- and slow down code review -- without any real performance gain. So, while this would be useful for golfed-down (!) one-liners with pyline, I'm -1 on PEP 572.
PEP 572 has never promised a performance gain, so "without any real performance gain" is hardly a criticism.
How do I step through this simple example with a debugger?
if re.search(pat, text) as match: print("Found:", match.group(0))
Step the 'if' statement. It will call re.search() and stash the result in 'match'. Then the cursor would be put either on the 'print' (if the RE matched) or on the next executable line (if it didn't).
Right. Pdb doesn't step through the AST branches of a line, so ternary expressions and list comprehensions and defining variables at the end of the line are 'debugged' after they're done.
Similarly, code coverage is line-based; so those expressions may appear to be covered but are not.
I'm not sure I follow. In what situation would some code appear to be covered when it isn't, due to an assignment expression?
When an assignment expression is in the else clause of a ternary expression, but the else clause does not execute because the condition is true, the assignment expression does not execute and so isn't covered.
if ((1) or (x := 3)): if ((y := func(x)) if x else (x := 3))
Is this a new opportunity for misunderstanding?
Assignment expressions, though they are noticeable :=, may not actually define the variable in cases where that part of the line doesn't run but reads as covered.
Okay. How is this different from anything else involving if/else expressions? If your code coverage checker is unable to handle if/else, it's unable to handle it whether there's an assignment in there or not. I don't understand why people bring up all these arguments that have absolutely nothing to do with the proposal at hand. None of this has in any way changed. ChrisA
[Chris Angelico <rosuav@gmail.com>]
... I don't understand why people bring up all these arguments that have absolutely nothing to do with the proposal at hand. None of this has in any way changed.
That's easy: any time there's a long thread to which Guido has contributed at least twice, it will be seen as a Golden Opportunity to re-litigate every decision that's ever been made ;-) Some amount of that seems healthy to me (people are thinking about "language design" from a larger view than the proposal du jour). In this specific case, line-oriented coverage tools have missed accounting for all possible code paths since day #1; e.g., x = f() or g() You don't need to reply to messages so obviously irrelevant to the PEP unless you want to. It's not like Guido will read them and go "oh! a binding expression in a ternary conditional is a fundamentally new potential problem for a line-oriented coverage tool! that's fatal" ;-)
On 04/27/2018 05:11 PM, Tim Peters wrote:
In this specific case, line-oriented coverage tools have missed accounting for all possible code paths since day #1; e.g.,
x = f() or g()
You don't need to reply to messages so obviously irrelevant to the PEP unless you want to. It's not like Guido will read them and go "oh! a binding expression in a ternary conditional is a fundamentally new potential problem for a line-oriented coverage tool! that's fatal" ;-) FWIW, Ned Batchelder's 'coverage.py' does a good job with branch coverage. I haven't seen anything in this discussion which indicates that binding expressions will change that at all.
Tres. -- =================================================================== Tres Seaver +1 540-429-0999 tseaver@palladion.com Palladion Software "Excellence by Design" http://palladion.com
[Tres Seaver <tseaver@palladion.com>]
FWIW, Ned Batchelder's 'coverage.py' does a good job with branch coverage. I haven't seen anything in this discussion which indicates that binding expressions will change that at all.
I don't think you missed anything relevant either ;-) Binding operators are exactly as irrelevant to control-flow analyzers as, e.g., introducing a floor division operator (//) was. Data-flow analyzers (if there are any for Python) are a different story, since they need to be aware of all (re)binding operations - although at the byte code level, all such sites remain equally apparent (no new flavor of "store" operation is added by this PEP).
On 4/27/18 5:28 PM, Tres Seaver wrote:
On 04/27/2018 05:11 PM, Tim Peters wrote:
In this specific case, line-oriented coverage tools have missed accounting for all possible code paths since day #1; e.g.,
x = f() or g()
You don't need to reply to messages so obviously irrelevant to the PEP unless you want to. It's not like Guido will read them and go "oh! a binding expression in a ternary conditional is a fundamentally new potential problem for a line-oriented coverage tool! that's fatal" ;-) FWIW, Ned Batchelder's 'coverage.py' does a good job with branch coverage. I haven't seen anything in this discussion which indicates that binding expressions will change that at all.
Coverage.py can measure branch coverage, but it is still line-oriented. There's no support for conditionals and branches within a line (though I've done some wicked hacks to experiment with it: https://nedbatchelder.com/blog/200804/wicked_hack_python_bytecode_tracing.ht...). It's entirely true that binding expressions don't change this situation at all, EXCEPT that the entire point of binding expressions is to be able to express in one statement what used to take more than one. With binding expressions, actions may be coverage-hidden within one statement that without them would have been coverage-visible in more than one statement. I'm not sure that's an argument against binding expressions, but we should at least acknowledge that the motivation for them is to provide the option to write fewer, longer statements. That option is not always a good idea, for a variety of reasons. --Ned.
On Sat, Apr 28, 2018 at 9:18 PM, Ned Batchelder <ned@nedbatchelder.com> wrote:
It's entirely true that binding expressions don't change this situation at all, EXCEPT that the entire point of binding expressions is to be able to express in one statement what used to take more than one. With binding expressions, actions may be coverage-hidden within one statement that without them would have been coverage-visible in more than one statement.
So far, all the examples in the PEP have the exact same coverage with and without assignment expressions, with a few exceptions where coverage is *improved* by them (where the alternative is to duplicate a function call). By combining multiple lines into one, we also ensure that all of it is executed exactly once, instead of having conditional execution. ChrisA
On Friday, April 27, 2018, Tim Peters <tim.peters@gmail.com> wrote:
[Chris Angelico <rosuav@gmail.com>]
... I don't understand why people bring up all these arguments that have absolutely nothing to do with the proposal at hand. None of this has in any way changed.
That's easy: any time there's a long thread to which Guido has contributed at least twice, it will be seen as a Golden Opportunity to re-litigate every decision that's ever been made ;-)
Some amount of that seems healthy to me (people are thinking about "language design" from a larger view than the proposal du jour). In this specific case, line-oriented coverage tools have missed accounting for all possible code paths since day #1; e.g.,
x = f() or g()
You don't need to reply to messages so obviously irrelevant to the PEP unless you want to. It's not like Guido will read them and go "oh! a binding expression in a ternary conditional is a fundamentally new potential problem for a line-oriented coverage tool! that's fatal" ;-)
I have shared with you the overlapping concerns about this feature proposal that I believe should be explained with DO and DON'T in the docs and/or the PEP and/or the style guide(s) for various organizations in the Pyrhon community. This feature does require additions to the style-guide(s); which is why so many have expressed concern about such a simple thing. If you want to write debuggable and coverage-testable code, do not use the assignment expression operator in ternary expressions or boolean-chained expressions. The assignment expression operator is the only way to define variables with only comprehension scope. Do not do this: x = 2 if (x == 3) or (x := 3): print(x) What do we call that mistake?
_______________________________________________ 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/ wes.turner%40gmail.com
Wes, sorry, but I really don't follow what you're saying. For example, [Wes Turner <wes.turner@gmail.com>]
Do not do this:
x = 2 if (x == 3) or (x := 3): print(x)
What do we call that mistake?
It displays 3 - while it appears to be silly code, there's nothing about it that's undefined. So I fail to see how showing that example anywhere would do anyone any good. You can do the same kind of thing today via, e.g., class Bindable: def __init__(self, value): self.bind(value) def bind(self, value): self.value = value return value def __bool__(self): return bool(self.value) def __eq__(self, other): return self.value == other def __str__(self): return str(self.value) Then:
x = Bindable(2) if x == 3 or x.bind(3): ... print(x) 3
And I wouldn't put that example anywhere in any docs either ;-)
It's certainly a contrived example. Actual code with such a mistake is generally far more subtle. The mistake is that it's assigning a value within a clause of a conditional that won't be evaluated. Oh well. I'll suffer the then worsened zig-zaggy eye movements in code reviews caused by defining values at the end of expressions that reference them which fit on a single line. There are a number of bad examples for style guides in these threads. := I wasn't aware of this switch, thanks! http://coverage.readthedocs.io/en/latest/branch.html coverage run --branch code.py On Friday, April 27, 2018, Tim Peters <tim.peters@gmail.com> wrote:
Wes, sorry, but I really don't follow what you're saying. For example,
[Wes Turner <wes.turner@gmail.com>]
Do not do this:
x = 2 if (x == 3) or (x := 3): print(x)
What do we call that mistake?
It displays 3 - while it appears to be silly code, there's nothing about it that's undefined. So I fail to see how showing that example anywhere would do anyone any good.
You can do the same kind of thing today via, e.g.,
class Bindable: def __init__(self, value): self.bind(value)
def bind(self, value): self.value = value return value
def __bool__(self): return bool(self.value)
def __eq__(self, other): return self.value == other
def __str__(self): return str(self.value)
Then:
x = Bindable(2) if x == 3 or x.bind(3): ... print(x) 3
And I wouldn't put that example anywhere in any docs either ;-)
On Sat, Apr 28, 2018 at 7:11 AM, Tim Peters <tim.peters@gmail.com> wrote:
[Chris Angelico <rosuav@gmail.com>]
... I don't understand why people bring up all these arguments that have absolutely nothing to do with the proposal at hand. None of this has in any way changed.
That's easy: any time there's a long thread to which Guido has contributed at least twice, it will be seen as a Golden Opportunity to re-litigate every decision that's ever been made ;-)
Well, now, that explains a lot! :-)
Some amount of that seems healthy to me (people are thinking about "language design" from a larger view than the proposal du jour). In this specific case, line-oriented coverage tools have missed accounting for all possible code paths since day #1; e.g.,
x = f() or g()
You don't need to reply to messages so obviously irrelevant to the PEP unless you want to. It's not like Guido will read them and go "oh! a binding expression in a ternary conditional is a fundamentally new potential problem for a line-oriented coverage tool! that's fatal" ;-)
True, but sometimes it takes two or three emails before I actually understand the objection enough to know that it's actually irrelevant :| I'm going to start ignoring any message that I don't understand, in the hopes that it doesn't actually mean anything. :| ChrisA
On Fri, Apr 27, 2018 at 04:24:35PM -0400, Wes Turner wrote:
if ((1) or (x := 3)): if ((y := func(x)) if x else (x := 3))
Wes, there is *absolutely nothing new* here. This sort of error is already possible in Python. Do you see a lot of code like: if (1 or sequence.append(3) or sequence[-1]): in real life? If you do, then I'm really, really sorry that you are forced to deal with such rubbish code, but honestly, the vast bulk of Python programmers do not write like that, and they won't write this either: if (1 or (x := 3)): [...]
Assignment expressions, though they are noticeable :=, may not actually define the variable in cases where that part of the line doesn't run but reads as covered.
The same applies to any operation at all. /sarcasm I guess adding print() to the language was a mistake, because we can write rubbish code like this: if 1 or print(x): and then be confused by the fact that x doesn't get printed. /end sarcasm In another post, you insisted that we need to warn in the PEP and the docs not to do this sort of thing. Should we also go through and add these warnings to list.append, dict.update, set.add, etc? I trust that the answer to that is obviously no. And neither do we have to assume that people who use binding-expressions will lose their minds and start writing the sort of awful code that they don't write with anything else. -- Steve
Łukasz Langa wrote:
Ha, not in Python! Here we have *different syntax* for assignments in expressions. Well, you can also use it as a statement. But don't!
This is what I least like about the proposal. We should be moving in the direction of removing warts, but here we would be *creating* one (two different assignment operators with overlapping use cases) that we won't be able to get rid of without a Python 4000 (that Guido has promised won't happen). -- Greg
On Apr 26, 2018, at 12:40 AM, Tim Peters <tim.peters@gmail.com> wrote:
[Raymond Hettinger <raymond.hettinger@gmail.com>]
After re-reading all the proposed code samples, I believe that adopting the PEP will make the language harder to teach to people who are not already software engineers.
Can you elaborate on that?
Just distinguishing between =, :=, and == will be a forever recurring discussion, far more of a source of confusion than the occasional question of why Python doesn't have embedded assignment. Also, it is of concern that a number of prominent core dev respondents to this thread have reported difficulty scanning the posted code samples.
I've used dozens of languages over the decades, most of which did have some form of embedded assignment.
Python is special, in part, because it is not one of those languages. It has virtues that make it suitable even for elementary school children. We can show well-written Python code to non-computer folks and walk them through what it does without their brains melting (something I can't do with many of the other languages I've used). There is a virtue in encouraging simple statements that read like English sentences organized into English-like paragraphs, presenting itself like "executable pseudocode". Perl does it or C++ does it is unpersuasive. Its omission from Python was always something that I thought Guido had left-out on purpose, intentionally stepping away from constructs that would be of help in an obfuscated Python contest.
Yes, I'm a software engineer, but I've always pitched in on "help forums" too.
That's not really the same. I've taught Python to many thousands of professionals, almost every week for over six years. That's given me a keen sense of what is hard to teach. It's okay to not agree with my assessment, but I would like for fruits of my experience to not be dismissed in a single wisp of a sentence. Any one feature in isolation is usually easy to explain, but showing how to combine them into readable, expressive code is another matter. And as Yuri aptly noted, we spend more time reading code than writing code. If some fraction of our users finds the code harder to scan because the new syntax, then it would be a net loss for the language. I hesitated to join this thread because you and Guido seemed to be pushing back so hard against anyone's who design instincts didn't favor the new syntax. It would be nice to find some common ground and perhaps stipulate that the grammar would grow in complexity, that a new operator would add to the current zoo of operators, that the visual texture of the language would change (and in a way that some including me do not find pleasing), and that while simplest cases may afford a small net win, it is a certitude that the syntax will routinely be pushed beyond our comfort zone. While the regex conditional example looks like a win, it is very modest win and IMHO not worth the overall net increase language complexity. Like Yuri, I'll drop-out now. Hopefully, you all wind find some value in what I had to contribute to the conversation. Raymond
[Raymond Hettinger <raymond.hettinger@gmail.com>]
After re-reading all the proposed code samples, I believe that adopting the PEP will make the language harder to teach to people who are not already software engineers.
[Tim]
Can you elaborate on that?
[Raymond]
Just distinguishing between =, :=, and == will be a forever recurring discussion, far more of a source of confusion than the occasional question of why Python doesn't have embedded assignment.
To be clear, is distinguishing between "=" and "==" already a forever recurring discussion in your experience? Or are you predicting that adding ":=" will create that situation?
Also, it is of concern that a number of prominent core dev respondents to this thread have reported difficulty scanning the posted code samples.
Yes, it is - although some of the examples sucked ;-)
I've used dozens of languages over the decades, most of which did have some form of embedded assignment.
Python is special, in part, because it is not one of those languages. It has virtues that make it suitable even for elementary school children. We can show well-written Python code to non-computer folks and walk them through what it does without their brains melting (something I can't do with many of the other languages I've used). There is a virtue in encouraging simple statements that read like English sentences organized into English-like paragraphs, presenting itself like "executable pseudocode".
It's certainly possible to stick to a subset of Python for which that's true. But I didn't mention those dozens of languages because I seek to emulate them, but to establish that I've had decades of experience with embedded assignments in a wide variety of languages and language communities.
Perl does it or C++ does it is unpersuasive.
Wasn't meant to be.
Its omission from Python was always something that I thought Guido had left-out on purpose, intentionally stepping away from constructs that would be of help in an obfuscated Python contest.
He left out lots of stuff at first, but warmed to it later. Probably the most profound: there were exactly and only 3 scopes at first: local, global, and builtin. Functions (for example) could still nest, but had no way to access names local to enclosing functions save via deep trickery. That was a noble experiment (it was a deliberate attempt to avoid complex scoping rules), but eventually proved too restrictive in practice. This is nothing compared to that ;-) But it's a tiny bit related in that biting the arbitrarily-deeply-nested-scopes bullet was aimed more at experienced programmers than at newbies. The scoping rules became far harder to explain as a result - but far more what experienced programmers expected.
Yes, I'm a software engineer, but I've always pitched in on "help forums" too.
That's not really the same.
I believe it!
I've taught Python to many thousands of professionals, almost every week for over six years. That's given me a keen sense of what is hard to teach. It's okay to not agree with my assessment, but I would like for fruits of my experience to not be dismissed in a single wisp of a sentence.
I asked you to elaborate - I didn't dismiss anything. You merely made a raw assertion in your original message, without enough detail to even know _what_ it is you thought would be hard to teach. Your elaboration is helping.
Any one feature in isolation is usually easy to explain, but showing how to combine them into readable, expressive code is another matter.
OK, so it's not binding expressions in isolation that you expect will be hard to teach if they're added, but ... how to use them intelligently (if ever)? That's progress, if so. That part I can see having major trouble with. Even the proponents of this PEP don't always agree with each other about which examples are "good ones".
And as Yuri aptly noted, we spend more time reading code than writing code. If some fraction of our users finds the code harder to scan because the new syntax, then it would be a net loss for the language.
It would be a tradeoff pitting their losses against others' gains, of course. I don't know how to quantify that (not even to the extent of determining the sign bit) in advance. I'm also at least as concerned about - indeed - professional software engineers as beginners.
I hesitated to join this thread because you and Guido seemed to be pushing back so hard against anyone's who design instincts didn't favor the new syntax.
That's just vigorous debate, at least on my part. Guido gets annoyed by emotional tirades and FUD, of which there's always plenty in threads that have gone on for hundreds of messages (I don't know whether you followed any of this on python-ideas, but most arguments on python-dev were already many-times-over old by the time it first appeared here).
It would be nice to find some common ground and perhaps stipulate that the grammar would grow in complexity, that a new operator would add to the current zoo of operators, that the visual texture of the language would change (and in a way that some including me do not find pleasing), and that while simplest cases may afford a small net win, it is a certitude that the syntax will routinely be pushed beyond our comfort zone.
While the regex conditional example looks like a win, it is very modest win and IMHO not worth the overall net increase language complexity.
Like Yuri, I'll drop-out now. Hopefully, you all wind find some value in what I had to contribute to the conversation.
Absolutely! While I've slowly moved from -1 to +1 on this one, I respect your -1, and am grateful for your thoughtful elaboration. Indeed, I'll feel better now when Guido rejects it ;-)
[Raymond Hettinger <raymond.hettinger@gmail.com>]
Python is special, in part, because it is not one of those languages. It has virtues that make it suitable even for elementary school children. We can show well-written Python code to non-computer folks and walk them through what it does without their brains melting (something I can't do with many of the other languages I've used). There is a virtue in encouraging simple statements that read like English sentences organized into English-like paragraphs, presenting itself like "executable pseudocode".
While this is true and good for most Python code, can you honestly explain asyncio code with async/await to these non-programmers?! What about the interfaces between async and synchronous portions? I've been programming for 40 years, in Python for 20 of them. I cannot read any block of async code without thinking VERY SLOWLY about what's going on, then getting it wrong half the time. I even teach Python almost as much as Raymond does. There's a certain hand-waving approach to teaching async/await where you say not to worry about those keywords, and just assume the blocks are coordinated "somehow, behind the scenes." That's not awful for reading *working* code, but doesn't let you write it. I'm not saying binding expressions are likewise reserved for a special but important style of programming. If included, I expect them to occur more-or-less anywhere. So Raymond's concern about teachability is more pressing (I've only taught async twice, and I know Raymond's standard course doesn't do it, all the other code is unaffected by that unused 'await' lurking in the syntax). Still, there are good reasons why not all Python code is aimed at non-computer folks.
Hi, On 26 April 2018 at 07:50, Raymond Hettinger <raymond.hettinger@gmail.com> wrote:
[Raymond Hettinger <raymond.hettinger@gmail.com>]
After re-reading all the proposed code samples, I believe that adopting the PEP will make the language harder to teach to people who are not already software engineers.
(...)
Python is special, in part, because it is not one of those languages. It has virtues that make it suitable even for elementary school children. We can show well-written Python code to non-computer folks and walk them through what it does without their brains melting (something I can't do with many of the other languages I've used). There is a virtue in encouraging simple statements that read like English sentences organized into English-like paragraphs, presenting itself like "executable pseudocode".
I must admit that when I heard about this PEP I thought "this April 1st joke was already done long ago". I'm sorry to discover that, this time, it is not actually one. Thank you, Raymond, for an unlikely attempt at reminding people what made Python so special---in your opinion, and mine. A bientôt, Armin.
Hi, 2018-04-28 5:08 GMT+02:00 Armin Rigo <armin.rigo@gmail.com>:
Hi,
On 26 April 2018 at 07:50, Raymond Hettinger <raymond.hettinger@gmail.com> wrote:
[Raymond Hettinger <raymond.hettinger@gmail.com>]
After re-reading all the proposed code samples, I believe that adopting the PEP will make the language harder to teach to people who are not already software engineers.
(...)
Python is special, in part, because it is not one of those languages. It has virtues that make it suitable even for elementary school children. We can show well-written Python code to non-computer folks and walk them through what it does without their brains melting (something I can't do with many of the other languages I've used). There is a virtue in encouraging simple statements that read like English sentences organized into English-like paragraphs, presenting itself like "executable pseudocode".
I must admit that when I heard about this PEP I thought "this April 1st joke was already done long ago". I'm sorry to discover that, this time, it is not actually one. Thank you, Raymond, for an unlikely attempt at reminding people what made Python so special---in your opinion, and mine.
A bientôt,
Armin.
Same feeling here. What I really appreciate of Python from long time is its readability: the fact that usually I read the code as English-like sentences. It was nice to see the usage of the "as" keyword in the try/except construct as well as in the with one, instead of introducing another bunch of symbols which will make it more difficult to decode the meaning of the writing. Same for the "if/else" ternary operator, which I read like "[give] x if cond else y", instead of the cryptic "?:" of C-like languages. It was a nice and wise design decision. For similar reasons, I did/don't like the @ for matrix multiplication because it doesn't give me any immediately, useful information which makes it easier to decode the meaning. A "mul" binary operator would have worked better, for example. I hope that Python core developers refuse the temptation to introduce new operators using symbols for new features: it's a short way to keep backward-compatibility, for sure, but if the price to pay is the readability, then I don't think that it's worth to do it. Regarding the assignment operator, I also find it a (bad, since it's not so much readable inside expressions) duplicate of the assignment statement. To be more precise, why should we keep the latter once with the former we can do the same things (and more)? Then drop the assignment statement and just leave the operator! BTW, as a pythonist I've also felt the need to have some way to "bind" values to variables in some context, but it's pretty much related to comprehensions, for obvious reasons I think. I would have appreciated an "as" keyword, only inside such constructs, but I don't see any value extending it for any generic context, since we already have the assignment statement which works quite well and doesn't introduce nasty side-effects "ala C-like languages". So, IMO it's better to stay as we are instead of introducing another kludge to the language, if we cannot maintain a good readability. Cheers, Cesare
On Wed, Apr 25, 2018 at 01:55:37PM -0700, Łukasz Langa wrote:
On 25 Apr, 2018, at 1:28 PM, Guido van Rossum <guido@python.org> wrote:
You don't seem to grasp the usability improvements this will give. I hear you but at this point appeals to Python's "Zen" don't help you.
This reads dismissive to me. I did read the PEP and followed the discussion on python-dev. I referred to PEP 20 because it distills what's unique about the value proposition of Python. It's our shared vocabulary.
Every programming language has a shared vocabulary. That's hardly unique to Python.
Can you address the specific criticism I had? To paraphrase it without PEP 20 jargon:
(name := expression) makes code less uniform. It inserts more information into a place that is already heavily packed with information (logic tests).
I'm not Guido, but I'll make an attempt. I think the comment about "less uniform" isn't meaningful. Uniform in what way? I don't even know how to interpret the uniform comment here, unless you mean to imply that every statement and expression in Python currently has the same information density, and binding-expressions will violate that. That's clearly not the case, so I'm left puzzled by what you mean. As for your observation that binding-expressions don't reduce complexity, they merely move it, I think you may be right. But then it is a truism that complexity is never reduced, only moved, so that's likely to be true for any feature (including existing ones). Should we move back to assembly language programming because Python hasn't reduced complexity, only moved it? I don't think so. Clearly binding-expressions do add a little more complexity to the language, and they do move code from vertically separated statements to horizontally laid-out expressions. But why is this necessarily a bad thing? Exactly the same complaint can be made about comprehensions, and look at how wildly successful they have been. Offset against the increase in horizontal complexity is a corresponding decrease in vertical complexity, and that's beneficial. Whatever cost they have has to be offset against the benefits, and I think the feature will come ahead on the plus side overall. Of course, like any syntactic feature, it may be abused by those who (by accident or design) write obfuscated or excessively complex code. We shouldn't ignore that risk, but nor should we use that as an excuse to dismiss the feature's benefits. -- Steve
On Wed, Apr 25, 2018 at 9:55 PM, Łukasz Langa <lukasz@langa.pl> wrote:
On 25 Apr, 2018, at 1:28 PM, Guido van Rossum <guido@python.org> wrote:
You don't seem to grasp the usability improvements this will give. I hear you but at this point appeals to Python's "Zen" don't help you.
This reads dismissive to me. I did read the PEP and followed the discussion on python-dev. I referred to PEP 20 because it distills what's unique about the value proposition of Python. It's our shared vocabulary.
Perhaps so, but no PEP is chiselled in stone, and I would suggest that
PEP 20 is the least authoritative from a didactic point of view.
Can you address the specific criticism I had? To paraphrase it without PEP 20 jargon:
(name := expression) makes code less uniform. It inserts more information into a place that is already heavily packed with information (logic tests).
One could argue the same about list comprehensions if one chose: they make code denser (by expressing the same algorithm in a shorter spelling). I'm not sure what you mean by "less uniform."
: On 25 April 2018 at 21:28, Guido van Rossum <guido@python.org> wrote:
A very emotional appeal, you don't seem to grasp the usability improvements this will give. I hear you but at this point appeals to Python's "Zen" don't help you.
I have to admit, in half-following this discussion I've swung between thinking "there's no way this is actually going to happen" and "what, this might actually happen?" Since it now looks like it really *does* have a decent chance, and maybe another -1 has a small chance of tipping the balance: my reaction to the proposal is also emotional. Visceral, in fact, to the extent that I'd aim to read and write less Python if it became commonplace. I don't have arguments. I just have an instinctive "ugh, no". -[]z.
On Thu, Apr 26, 2018 at 08:00:46PM +0100, Zero Piraeus wrote:
Since it now looks like it really *does* have a decent chance, and maybe another -1 has a small chance of tipping the balance: my reaction to the proposal is also emotional. Visceral, in fact, to the extent that I'd aim to read and write less Python if it became commonplace.
Funnily enough, that's what some people said about decorator syntax, ternary if, type annotations and list comprehensions. All of them have become great additions to the language. I hated the idea of aping C and adding += operators and swore I'd never use them. That lasted, well, about a month. Just sayin'. -- Steve
[Zero Piraeus]
Since it now looks like it really *does* have a decent chance, and maybe another -1 has a small chance of tipping the balance: my reaction to the proposal is also emotional. Visceral, in fact, to the extent that I'd aim to read and write less Python if it became commonplace.
[Steven D'Aprano <steve@pearwood.info>]
Funnily enough, that's what some people said about decorator syntax, ternary if, type annotations and list comprehensions.
All of them have become great additions to the language.
I hated the idea of aping C and adding += operators and swore I'd never use them. That lasted, well, about a month.
Just sayin'.
Well - I've come to respect your opinion, so ... OK, I'll give += a try. Frankly, I've grown tired of editing it out of all the packages I download anyway ;-)
Łukasz Langa wrote:
What was its own assignment before now is part of the logic test. This saves on vertical whitespace but makes parsing and understanding logic tests harder.
Another way to say this is that expressions are no longer restricted to being trees, but can be general DAGs, which require more mental effort to understand. Hmmm, maybe they should be called "spaghetti expressions". :-) -- Greg
On Thu, Apr 26, 2018 at 05:22:58PM +1200, Greg Ewing wrote:
Łukasz Langa wrote:
What was its own assignment before now is part of the logic test. This saves on vertical whitespace but makes parsing and understanding logic tests harder.
Another way to say this is that expressions are no longer restricted to being trees, but can be general DAGs, which require more mental effort to understand.
Is that right? I presume you mean that there can be cycles in expressions involving binding-expressions. If not, what do you mean? Can you give an example of a Python expression, involving PEP 572 binding-expressions, that is not a tree but a more general DAG or that contains cycles? -- Steve
On Thu, Apr 26, 2018 at 3:34 PM, Steven D'Aprano <steve@pearwood.info> wrote:
On Thu, Apr 26, 2018 at 05:22:58PM +1200, Greg Ewing wrote:
Łukasz Langa wrote:
What was its own assignment before now is part of the logic test. This saves on vertical whitespace but makes parsing and understanding logic tests harder.
Another way to say this is that expressions are no longer restricted to being trees, but can be general DAGs, which require more mental effort to understand.
Is that right? I presume you mean that there can be cycles in expressions involving binding-expressions. If not, what do you mean?
Can you give an example of a Python expression, involving PEP 572 binding-expressions, that is not a tree but a more general DAG or that contains cycles?
A DAG is a directed *acyclic* graph, so it still can't contain cycles. But I have no idea what kind of expression isn't a tree as a consequence of having an assignment in it. ChrisA
On 26 April 2018 at 15:38, Chris Angelico <rosuav@gmail.com> wrote:
On Thu, Apr 26, 2018 at 3:34 PM, Steven D'Aprano <steve@pearwood.info> wrote:
Can you give an example of a Python expression, involving PEP 572 binding-expressions, that is not a tree but a more general DAG or that contains cycles?
A DAG is a directed *acyclic* graph, so it still can't contain cycles. But I have no idea what kind of expression isn't a tree as a consequence of having an assignment in it.
At a parsing level, the expression remains a tree, it's just that one of the nodes is a name lookup. At a logical level though, binding expressions do indeed mean that expressions involving binding expressions are at least arguably better modelled with a DAG rather than as a tree the way they are now: # The arithmetic expression is a tree including two nodes that look up "c". The DAG is only needed at a statement level. c = expr() a*c + b*c # Whereas this backref to "c" pulls the DAG down to the expression level a*(c := expr()) + b*c Historically, that kind of order-of-evaluation dependence in Python has only been a problem for functions with side effects, so the folks asking that this be seen as a major complexity increase for expression level semantics have an entirely valid point. The PEP aims to address that point by saying "Don't use binding expressions when the order of evaluation would be ambiguous", but that's as a response to a valid concern, not a dismissal of it. Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia
On Thu, 26 Apr 2018 15:34:17 +1000 Steven D'Aprano <steve@pearwood.info> wrote:
On Thu, Apr 26, 2018 at 05:22:58PM +1200, Greg Ewing wrote:
Łukasz Langa wrote:
What was its own assignment before now is part of the logic test. This saves on vertical whitespace but makes parsing and understanding logic tests harder.
Another way to say this is that expressions are no longer restricted to being trees, but can be general DAGs, which require more mental effort to understand.
Is that right? I presume you mean that there can be cycles in expressions involving binding-expressions. If not, what do you mean?
Can you give an example of a Python expression, involving PEP 572 binding-expressions, that is not a tree but a more general DAG or that contains cycles?
Depends if you mean a graph between names or values? If between names, you can even have cycles AFAICT: ((a: = a + b), (b: = a)) Regards Antoine.
Antoine Pitrou wrote:
Depends if you mean a graph between names or values?
If between names, you can even have cycles AFAICT:
((a: = a + b), (b: = a))
I was thinking more of the dataflow graph. That's not a cycle between *values*, since the new values being bound are calculated from the previous values of the names. -- Greg
Reading this sub-thread, it struck me that a good way to make PEP 562 more likely to be accepted is to launch an over-the-top attack on it. Then more moderate people - who were/are not necessarily in favour of the PEP - feel pressurised into defending it. Hah! Watch this space for my vicious, vitriolic, withering attack on PEP 463 (Exception-catching expressions)! :-) Best wishes Rob Cliffe
participants (26)
-
Antoine Pitrou
-
Armin Rigo
-
Cesare Di Mauro
-
Chris Angelico
-
Chris Barker
-
David Mertz
-
Ethan Furman
-
Glenn Linderman
-
Greg Ewing
-
Guido van Rossum
-
Kirill Balunov
-
Lukasz Langa
-
Ned Batchelder
-
Nick Coghlan
-
Raymond Hettinger
-
Rob Cliffe
-
Stephen J. Turnbull
-
Steve Holden
-
Steven D'Aprano
-
Terry Reedy
-
Tim Peters
-
Tres Seaver
-
Wes Turner
-
Yury Selivanov
-
Zero Piraeus
-
Łukasz Langa