python / cpython

The Python programming language
https://www.python.org
Other
63.4k stars 30.36k forks source link

Allow multiple assignment (i.e. tuple on LHS) in walrus operator #87309

Open 02ad864c-ae2a-4ade-b93d-57afad65b191 opened 3 years ago

02ad864c-ae2a-4ade-b93d-57afad65b191 commented 3 years ago
BPO 43143
Nosy @gvanrossum, @tim-one, @rhettinger, @pfalcon, @emilyemorehouse, @lysnikolaou

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'] ```

02ad864c-ae2a-4ade-b93d-57afad65b191 commented 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.

rhettinger commented 3 years ago

+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).

02ad864c-ae2a-4ade-b93d-57afad65b191 commented 3 years ago

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).

andylamp commented 10 months ago

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!

KaiPetzke commented 7 months ago

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.

andylamp commented 7 months ago

@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.

huili80 commented 5 months ago

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

  1. The result of an assignement expression is the expression on the right-hand side, which is actually stated in the doc (https://docs.python.org/3/reference/expressions.html#grammar-token-python-grammar-assignment_expression).
  2. The expression is evaluated before the assignment, and is not re-evaluated after the assignment, which obviously makes sense.

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.

p4l1ly commented 4 months ago

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]
KaiPetzke commented 2 months ago

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.

p4l1ly commented 2 months ago

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.