Open 02ad864c-ae2a-4ade-b93d-57afad65b191 opened 3 years ago
Currently (CPython 3.10.0a4) having a tuple on left-hand side of assignment expression/operator (aka walrus operator) is not allowed:
>>> ((a, b) := (1, 2))
File "<stdin>", line 1
((a, b) := (1, 2))
^
SyntaxError: cannot use assignment expressions with tuple
As far as can tell, there's no explicit consideration of this case in the PEP-572. There closest it has is under the https://www.python.org/dev/peps/pep-0572/#differences-between-assignment-expressions-and-assignment-statements section, "Iterable packing and unpacking (both regular or extended forms) are not supported".
But note that the usecase presented above is better seen as "parallel assignment", not as "iterable unpacking".
Next issue: PEP-572 puts heavy emphasis on the "named expression" usecase. I would like to emphasize, that "naming an expression" seems like *just one of usecases* for assignment operator. First and foremost assignment operator is, well, assignment.
With that in mind, ((a, b) := (1, 2)) is compatible with "naming expressions" idea, it "gives names" to multiple expressions at once.
There's a temptation to suggest that the above could be rewritten as:
((a := 1), (b := 2))
, and for the "naming" usecase, that would be true. But as noted above, "naming" is just *one* of the usecases. Following can't be "sequentialized" like that:
((b, a) := (a, b))
And for as long as someone considers the following acceptable:
func(a := val)
There's little point to prohibit the following:
min((b, a) := (a, b))
And that's the main argument - consistency between assignment statement and assignment operator. For as long as this is allowed:
a, b = b, c
it makes sense to allow:
((a, b) := (b,c))
(Extra parens account for different in grammar and precedence for the operator).
This issue is posted to capture discussion initially posted to python-ideas/python-dev:
https://mail.python.org/archives/list/python-ideas@python.org/thread/6IFDG7PAFPHVPGULANOQDAHP2X743HCE/ https://mail.python.org/archives/list/python-ideas@python.org/thread/X3LNCCO6PHTLAQXHEAP6T3FFW5ZW5GR5/ https://mail.python.org/archives/list/python-dev@python.org/thread/6IFDG7PAFPHVPGULANOQDAHP2X743HCE/
, to serve as a reference to whoever may be searching the bugtracker for this case.
+0 I think this is with worth considering. Whenever the use case arises, the workaround is awkward. Code would be clearer if assignment expressions were liberalized to allow unpacking.
The PEP aspired to avoid complex lvalues that might be hard to read or that had tricky semantics. Unpacking is one the simpler cases and would be mostly harmless and sometimes beneficial (and not just for the uncommon case of wanting simultaneous assignment).
Thanks.
I would like to put this ticket in the context of other grammar-related tickets/elaboration for the assignment operator:
https://bugs.python.org/issue42316 - allow foo[a:=1] instead of foo[(a:=1)] https://bugs.python.org/issue42374 - allow (c := i for i in b) instead of ((c := i) for i in b) https://bugs.python.org/issue42381 - allow {i := 0 for i in b} instead of {(i := 0) for i in b}
All of the above were implemented. Of course, allow parallel assignment, which was previously disabled, is somewhat a bigger change then allowing unparenthesized assignment usage. But from the grammar point of view, they are of the same nature (using walrus-aware term at right place). The initial patch I have on my hands is:
named_expression[expr_ty]:
- | a=NAME ':=' ~ b=expression { _Py_NamedExpr(CHECK(expr_ty, _PyPegen_set_expr_context(p, a, Store)), b, EXTRA) }
+ | a=star_targets ':=' ~ b=expression { _Py_NamedExpr(CHECK(expr_ty, _PyPegen_set_expr_context(p, a, Store)), b, EXTRA) }
And let's review the 'foo[(a := 1)]' case from more angles. C had assignment as expression from the very beginning, but in all fairness, I never saw "foo[a = 1]" in my whole life (well, maybe in https://www.ioccc.org/ submissions). But we see that with Python, people are not too shy to use that. And they even submit issues like "hey, I don't want to write foo[(a := 1)], I want to write foo[a := 1] !" And they don't get replies like "you know, doing assignment in index would hamper readability/maintainability; and those extra parens are there exactly to hint you more about this fact". Instead, the case is getting acked and fixed.
So, under such circumstances, I don't think that writing "min((a, b) := (b, a))" should be considered "much worse" than "foo[a := 1]", and should be kept disallowed (as again, fix for both is of the same nature).
Just commenting there, one of the major benefits of the walrus operator is to make if cases with assignment and checking easier and more readable.
Let us consider this case,
if ((a, b) := foo()) and (a > 1 or b < 2):
# do stuff
Currently statements like that fail, which is a shame... is it still not considered a valid use case? If so, are there any suggestions to make such cases prettier and more compact?
Thanks!
The use case suggested by @andylamp can currently be written as follows:
if (tuple := foo()) and ((a := tuple[0]) > 1 or (b := tuple[1]) < 2):
# do stuff
The Python team has been slow to accept the walrus operator into the language to avoid ambiguities and hard to read source code. But ambiguities will directly arise, when the left- and right-hand side of the walrus operator are of different type in a compound assignment expression. For example, should the following call print ab
according to the right-hand side, which is a two-byte string, or ("a", "b")
according to the re-assembled left-hand side, which would be a two-element tuple of one-byte strings:
print(((a, b) := "ab"))
And what would the next statement print? (1, 2, 3, 4)
from the right-hand side or a piece-wise reconstructed left-hand side, or (4, 4, 3, 4)
from reconstructing the left-hand side only after the assignment is complete?
print(((dummy, dummy, x, dummy) := (1, 2, 3, 4)))
And things become even more complex, when iterators with side effects come into the show.
@KaiPetzke yep, I am aware this can be achieved that way; however it is a bit verbose compared to the vanilla walrus one.
I also understand the difficulties integrating the walrus operator in the base language, but wanted to flag this valid use-case.
The use case suggested by @andylamp can currently be written as follows:
if (tuple := foo()) and ((a := tuple[0]) > 1 or (b := tuple[1]) < 2): # do stuff
The Python team has been slow to accept the walrus operator into the language to avoid ambiguities and hard to read source code. But ambiguities will directly arise, when the left- and right-hand side of the walrus operator are of different type in a compound assignment expression. For example, should the following call print
ab
according to the right-hand side, which is a two-byte string, or("a", "b")
according to the re-assembled left-hand side, which would be a two-element tuple of one-byte strings:print(((a, b) := "ab"))
And what would the next statement print?
(1, 2, 3, 4)
from the right-hand side or a piece-wise reconstructed left-hand side, or(4, 4, 3, 4)
from reconstructing the left-hand side only after the assignment is complete?print(((dummy, dummy, x, dummy) := (1, 2, 3, 4)))
And things become even more complex, when iterators with side effects come into the show.
None of these considerations are unique to having a tuple on the left-hand side, consider this example,
print(a := (a := 1, a + 1), a)
, which is currently allowed, and the result is (1, 2) (1, 2)
.
So it appears clear that
Given the above observed behavior, I'm not sure there would be ambiguity at all that could result from allowing a tuple on the left-hand side of the walrus operator.
print(((a, b) := "ab"))
should print "ab"
print(((dummy, dummy, x, dummy) := (1, 2, 3, 4)))
should print (1, 2, 3, 4)
This would also enable more descriptive (and maybe more performing) let
statements in list comprehensions. With the following definition,
def let(x):
return True
one could write
xs = [x for ab in abs if let((x,y) := ab2xy(ab)) if y]
instead of
xs = [x for ab in abs for x, y in (ab2xy(ab),) if y]
one could write
xs = [x for ab in abs if let((x,y) := ab2xy(ab)) if y]
One can already write today:
xs = [ab2y_res[0] for ab in abs if (ab2y_res := ab2y(ab)) and ab2y_res[1]]
It is clearly a bit more verbose than the walrus double assignment. On the other hand, assigning the result of ab2y()
to just one variable and then indexing it, will likely be more readable to someone, who has to read someone else's code, as only one new temporary variable is involved instead of two.
I don't agree. Variables with reasonable names are more readable than positional indexing. Also, in a more complex case, one loses the main idea of the code in a sea of brackets.
Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.
Show more details
GitHub fields: ```python assignee = None closed_at = None created_at =
labels = ['interpreter-core', '3.10']
title = 'Allow multiple assignment (i.e. tuple on LHS) in walrus operator'
updated_at =
user = 'https://github.com/pfalcon'
```
bugs.python.org fields:
```python
activity =
actor = 'pfalcon'
assignee = 'none'
closed = False
closed_date = None
closer = None
components = ['Interpreter Core']
creation =
creator = 'pfalcon'
dependencies = []
files = []
hgrepos = []
issue_num = 43143
keywords = []
message_count = 3.0
messages = ['386551', '386564', '386620']
nosy_count = 6.0
nosy_names = ['gvanrossum', 'tim.peters', 'rhettinger', 'pfalcon', 'emilyemorehouse', 'lys.nikolaou']
pr_nums = []
priority = 'normal'
resolution = None
stage = None
status = 'open'
superseder = None
type = None
url = 'https://bugs.python.org/issue43143'
versions = ['Python 3.10']
```