Re: [Python-Dev] Re: What's up with assignment expression and tuples?
Hello, Thanks for the reply. On Fri, 5 Feb 2021 13:32:25 -0500 Terry Reedy <tjreedy@udel.edu> wrote:
On 2/5/2021 2:51 AM, Paul Sokolovsky wrote:
((a, b) := (1, 2)) File "<stdin>", line 1 SyntaxError: cannot use assignment expressions with tuple
Why this accidental syntactic gap?
As should be clear from reading "Differences between assignment expressions and assignment statements", this 'gap' in entirely intentional, not accidental. *All* elaborations of 'name := expression' are listed and rejected as outside the scope of the proposal, which was to keep one reference to the expression value for later use. At least some of these elaborations were suggested and rejected during the voluminous discussion.
And looking back now, that seems like intentionally added accidental gap in the language (https://en.wikipedia.org/wiki/Accidental_gap). Similar to artificially limiting decorator syntax, which was already un-limited. But seems, there're no "lessons learned", and there's now need to wait a decade again before fixing that?
The principal a.e. use in conditional expressions is testing for non-nullness. Your
while ((a1, b1) := phi([a0, a2], [b0, b2]))[0] < 5: a2 = a1 + 1 b2 = b1 + 1
is an unusual and very specific use.
Well, many people were thinking (and I bet still think) that ":=" itself is very unusual case. But if it's in, why not make it consistent with the assignment statement and unleash the full power of it?
You want to have your tuple (for subscripting for testing) and eat it too (by unpacking).
That's good characterization, thanks. And Python syntax alone would allow to do that, if not extra-syntactical limitations put on top of it.
One can instead separate unpacking from testing a couple of ways
while (tup := phi([a0, a2], [b0, b2]))[0] < 5: a2, b2 = tup a2 = a1 + 1 b2 = b1 + 1
while True: a1, b1 = phi([a0, a2], [b0, b2]) if a1 >= 5: break a2 = a1 + 1 b2 = b1 + 1
Right, but I started my original email with "I finally found a usecase where *not* using assignment expression is *much* worse than using it." Both conversions above apply additional disturbances to the original program, beyond pure SSA conversion, which is quite a disturbance on its own. I was really excited that Python 3.7+ would be *the* language which would allow to express SSA conversion faithfully on the source form of the high-level language (usually SSA is applied to low-level assembly-like intermediate representation). But oops, accidental gap, and despite all the Python advanceness, still need to apply workarounds as with other mundane languages. [] -- Best regards, Paul mailto:pmiscml@gmail.com
On Sat, Feb 6, 2021 at 6:08 AM Paul Sokolovsky <pmiscml@gmail.com> wrote:
And looking back now, that seems like intentionally added accidental gap in the language (https://en.wikipedia.org/wiki/Accidental_gap). Similar to artificially limiting decorator syntax, which was already un-limited. But seems, there're no "lessons learned", and there's now need to wait a decade again before fixing that?
Lesson learned: Early limitations ARE easily lifted, so it's still a good idea to limit them until we've seen how they get used in practice. :)
The principal a.e. use in conditional expressions is testing for non-nullness. Your
while ((a1, b1) := phi([a0, a2], [b0, b2]))[0] < 5: a2 = a1 + 1 b2 = b1 + 1
is an unusual and very specific use.
Well, many people were thinking (and I bet still think) that ":=" itself is very unusual case. But if it's in, why not make it consistent with the assignment statement and unleash the full power of it?
TBH I don't think I'm understanding the use-case here, because it looks like it'd work just fine with fewer variables and a cleaner rendition: while a1 < 5: a1, b1 = phi([a0, a1], [b0, b1]) a1 += 1 b1 += 1
Right, but I started my original email with "I finally found a usecase where *not* using assignment expression is *much* worse than using it."
"Much worse" doesn't fit with what I would write that code as, so I must have misunderstood your code somewhere. ChrisA
Hello, On Sat, 6 Feb 2021 06:17:08 +1100 Chris Angelico <rosuav@gmail.com> wrote:
On Sat, Feb 6, 2021 at 6:08 AM Paul Sokolovsky <pmiscml@gmail.com> wrote:
And looking back now, that seems like intentionally added accidental gap in the language (https://en.wikipedia.org/wiki/Accidental_gap). Similar to artificially limiting decorator syntax, which was already un-limited. But seems, there're no "lessons learned", and there's now need to wait a decade again before fixing that?
Lesson learned: Early limitations ARE easily lifted, so it's still a good idea to limit them until we've seen how they get used in practice. :)
Well, I wish there was also a bit of "try to make things consistent from the beginning", because "easily lifted" is a bit optimistic characterization. That said, that's meta-level thought, down to earth case is that I indeed would like to start talks on actually extending walrus syntax to accept tuple/list on LHS. I've now triaged https://bugs.python.org/issue?%40columns=id%2Cactivity%2Ctitle%2Ccreator%2Cassignee%2Cstatus%2Ctype&%40sort=-activity&%40filter=status&%40action=searchid&ignore=file%3Acontent&%40search_text=walrus&submit=search&status=-1%2C1%2C2%2C3 and see that the only case it was mention is https://bugs.python.org/issue42295 , which was a case of walrus precedence confusion. I thus submitted https://bugs.python.org/issue43143 for future reference.
The principal a.e. use in conditional expressions is testing for non-nullness. Your
while ((a1, b1) := phi([a0, a2], [b0, b2]))[0] < 5: a2 = a1 + 1 b2 = b1 + 1
is an unusual and very specific use.
Well, many people were thinking (and I bet still think) that ":=" itself is very unusual case. But if it's in, why not make it consistent with the assignment statement and unleash the full power of it?
TBH I don't think I'm understanding the use-case here,
I hope we understand that we go into rabbit hole of discussing a specific case, whereas I reported a general limitation, and tried to back it with more or less general example (i.e. not a once-off case, with the "SSA conversion" usecase, each and every while would be affected). With that in mind, I guess it's fine for python-ideas to discuss that particular usecase in detail.
because it looks like it'd work just fine with fewer variables and a cleaner rendition:
while a1 < 5: a1, b1 = phi([a0, a1], [b0, b1]) a1 += 1 b1 += 1
Let's first note that you omitted the initialization of a0 and b0 which was in my example. And with that in mind, you'll immediately see what's wrong with your code: it tries to access uninitialized a1. That's on first loop iteration. On next, it will test *old* value of a1, whereas it should test already updated value. That's why it really has to assign new values in the loop's condition expression: while ((a1, b1) := phi([a0, a2], [b0, b2]))[0] < 5: (of course, if you're careful, you'll notice that a2, b2 are initialized on 1st iter, so assume there're initialized as: a2 = b2 = __undef__ Where __undef__ causes exception on any operation with it. )
Right, but I started my original email with "I finally found a usecase where *not* using assignment expression is *much* worse than using it."
"Much worse" doesn't fit with what I would write that code as, so I must have misunderstood your code somewhere.
*My* motivation is "SSA is already pretty complex conversion, so it would be nice to avoid *additional* Python source transformations not related to SSA per se, especially if Python generic syntax now allows that". But, again, it's just an example case. I use it to illustrate an apparent gap in ":=" functionality comparing to "=", and propose to lift that restriction.
ChrisA
[] -- Best regards, Paul mailto:pmiscml@gmail.com
While we're on the subject of assignment expression limitations, I've occasionally wanted to write something like try: return a_dict[key] except KeyError: return (a_dict[key] := expression to construct value)
On Sat, Feb 6, 2021 at 5:21 PM Random832 <random832@fastmail.com> wrote:
While we're on the subject of assignment expression limitations, I've occasionally wanted to write something like
try: return a_dict[key] except KeyError: return (a_dict[key] := expression to construct value)
That's what the __missing__ method is for. ChrisA
Hello, On Sat, 6 Feb 2021 17:26:00 +1100 Chris Angelico <rosuav@gmail.com> wrote:
On Sat, Feb 6, 2021 at 5:21 PM Random832 <random832@fastmail.com> wrote:
While we're on the subject of assignment expression limitations, I've occasionally wanted to write something like
try: return a_dict[key] except KeyError: return (a_dict[key] := expression to construct value)
That's what the __missing__ method is for.
That's true, but that would be an argument against PEP572 ;-). But we live in a world where PEP572 is a reality, and people start to look how to get the most juice out of it. While triaging the issue with parallel assignments, I saw there's already pretty long trail of elaboration of ":=" for various cases where original implementation forced parens, which were looking weird to human eye, e.g.: https://bugs.python.org/issue42316 https://bugs.python.org/issue42374 https://bugs.python.org/issue42381 My proposal is to file parallel assignment case into the same "inital omission" department like the above. And just like the above, it seems it would be a trivial grammar fix (I didn't run the whole testsuite on the result yet though ;-) ). An orthogonal case brought up in the above quoted message, of allowing Attribute/Subscript on LHS of the walrus, also seems legit to me, but I'm not sure about impl effort (but my guess it's again would be trivial or small, as apparently codegeneration paths are shared for "=" and ":=").
ChrisA
-- Best regards, Paul mailto:pmiscml@gmail.com
On Sat, Feb 6, 2021 at 8:54 PM Paul Sokolovsky <pmiscml@gmail.com> wrote:
Hello,
On Sat, 6 Feb 2021 17:26:00 +1100 Chris Angelico <rosuav@gmail.com> wrote:
On Sat, Feb 6, 2021 at 5:21 PM Random832 <random832@fastmail.com> wrote:
While we're on the subject of assignment expression limitations, I've occasionally wanted to write something like
try: return a_dict[key] except KeyError: return (a_dict[key] := expression to construct value)
That's what the __missing__ method is for.
That's true, but that would be an argument against PEP572 ;-).
PEP 572 was never meant to replace all other ways of doing things, and __missing__ is exactly the tool for this job :)
But we live in a world where PEP572 is a reality, and people start to look how to get the most juice out of it.
Why? Just because it's there?
While triaging the issue with parallel assignments, I saw there's already pretty long trail of elaboration of ":=" for various cases where original implementation forced parens, which were looking weird to human eye, e.g.:
https://bugs.python.org/issue42316 https://bugs.python.org/issue42374 https://bugs.python.org/issue42381
My proposal is to file parallel assignment case into the same "inital omission" department like the above. And just like the above, it seems it would be a trivial grammar fix (I didn't run the whole testsuite on the result yet though ;-) ).
The omission was intentional, and so far, I'm not seeing a justification for reversing that decision :) ChrisA
On 2021-02-05 22:18, Random832 wrote:
While we're on the subject of assignment expression limitations, I've occasionally wanted to write something like
try: return a_dict[key] except KeyError: return (a_dict[key] := expression to construct value)
You can already do that with `return a_dict.setdefault(key, your_expression_here)`. If the expression is expensive to evaluate you can use a short-circuiting conditional expression to guard it. -- Brendan Barnwell "Do not follow where the path may lead. Go, instead, where there is no path, and leave a trail." --author unknown
On Sat, Feb 6, 2021 at 5:21 PM Random832 <random832@fastmail.com> wrote:
While we're on the subject of assignment expression limitations, I've occasionally wanted to write something like
try: return a_dict[key] except KeyError: return (a_dict[key] := expression to construct value)
On Sat, Feb 6, 2021, at 01:26, Chris Angelico wrote:
That's what the __missing__ method is for.
Requires a dict subclass Requires that the value be determinable from the key alone [rather than any other variables available in the function containing the above code] On Sat, Feb 6, 2021, at 01:50, Brendan Barnwell wrote:
You can already do that with `return a_dict.setdefault(key, your_expression_here)`. If the expression is expensive to evaluate you can use a short-circuiting conditional expression to guard it.
How exactly would you use a short-circuiting conditional expression to do this? If you're suggesting `... if key not in a_dict else ...` this creates a race condition, and also involves checking the key in the dictionary twice. Perhaps a constructdefault method could be added to dict, broadly equivalent to def constructdefault(self, func): try: return self[key] except KeyError: return (self[key] := func()) you could use self[key] = value = func(); return value; and the same in the original [not part of dict class] snippet I posted above, but the point is this seems so much like exactly the sort of use case that := is intended for, that it comes across as weird that it's not usable.
On Mon, Feb 8, 2021 at 4:21 PM Random832 <random832@fastmail.com> wrote:
On Sat, Feb 6, 2021 at 5:21 PM Random832 <random832@fastmail.com> wrote:
While we're on the subject of assignment expression limitations, I've occasionally wanted to write something like
try: return a_dict[key] except KeyError: return (a_dict[key] := expression to construct value)
On Sat, Feb 6, 2021, at 01:26, Chris Angelico wrote:
That's what the __missing__ method is for.
Requires a dict subclass
That shouldn't be a big deal in most situations.
Requires that the value be determinable from the key alone [rather than any other variables available in the function containing the above code]
True, but in many situations that IS the case. You're stipulating a situation where: 1) The construction isn't a simple no-arg function (so you can't use defaultdict) 2) The construction can't be figured out from the key alone 3) The cost of construction is high (so you can't use setdefault) 4) There are multiple places where you'd want this behaviour (so you can't just try/except it) Seems a tad narrow to me. Got an actual use-case for this?
On Sat, Feb 6, 2021, at 01:50, Brendan Barnwell wrote:
You can already do that with `return a_dict.setdefault(key, your_expression_here)`. If the expression is expensive to evaluate you can use a short-circuiting conditional expression to guard it.
How exactly would you use a short-circuiting conditional expression to do this?
If you're suggesting `... if key not in a_dict else ...` this creates a race condition, and also involves checking the key in the dictionary twice.
Only against deletion. You could use: a_dict[key] if key in a_dict else a_dict.setdefault(key, big_long_epr) and you're guaranteed that it will use the existing value if it exists. It's possible that the key exists at the condition and then has been deleted before the subscript, but then you have to ask what would happen if you did a regular setdefault and then the key was deleted, and at this point, it's entirely down to the application's semantics (what does it MEAN if something's been deleted while something else is trying to auto-add it?). Checking if a key is in a dict is extremely cheap. Dicts are fundamental to Python, and every implementation will have a highly optimized dict such that you shouldn't need to worry about that kind of cost.
Perhaps a constructdefault method could be added to dict, broadly equivalent to
def constructdefault(self, func): try: return self[key] except KeyError: return (self[key] := func())
you could use self[key] = value = func(); return value; and the same in the original [not part of dict class] snippet I posted above, but the point is this seems so much like exactly the sort of use case that := is intended for, that it comes across as weird that it's not usable.
During discussion of PEP 463, one of the points raised was that many situations like this might be better spelled as exception expressions. That was largely shot down by the fact that, in actual usage, methods like setdefault are FAR easier to work with. I think the same applies here; rather than shoehorning assignment expressions into this job, figure out why it is that both __missing__ and setdefault() aren't suitable, and see if THAT problem can be solved. I've been using assignment expressions since they were added to the language (my primary Python interpreter has, for a number of years, been a master branch build), and they've been incredibly useful, but I have never once run into a situation where the restrictions cause me problems. ChrisA
participants (4)
-
Brendan Barnwell
-
Chris Angelico
-
Paul Sokolovsky
-
Random832