Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why aren't augmented assignment expressions allowed?

Tags:

python

I was recently reading over PEP 572 on assignment expressions and stumbled upon an interesting use case:

# Compute partial sums in a list comprehension
total = 0
partial_sums = [total := total + v for v in values]
print("Total:", total)

I began exploring the snippet on my own and soon discovered that :+= wasn't valid Python syntax.

# Compute partial sums in a list comprehension
total = 0
partial_sums = [total :+= v for v in values]
print("Total:", total)

I suspect there may be some underlying reason in how := is implemented that wisely precludes :+=, but I'm not sure what it could be. If someone wiser in the ways of Python knows why :+= is unfeasible or impractical or otherwise unimplemented, please share your understanding.

like image 919
FountainTree Avatar asked Sep 29 '21 18:09

FountainTree


People also ask

What are augmented assignment operators how are they useful?

Augmented assignment is the combination, in a single statement, of a binary operation and an assignment statement. e.g. a += 1 means a is assigned a+1. They are useful because they reduce the length of the code and make it look cleaner.

What addition method is used for augmented assignment?

Augmented Assignment Is a Statement The most common form of an augmented assignment that many are familiar with is += .

What is augmented assignment operator Python?

Augmented assignment operators have a special role to play in Python programming. It basically combines the functioning of the arithmetic or bitwise operator with the assignment operator.

What is assignment expression?

Assignment expressions allow you to assign and return a value in the same expression. For example, if you want to assign to a variable and print its value, then you typically do something like this: >>> >>> walrus = False >>> print(walrus) False.


2 Answers

The short version: The addition of the walrus operator was incredibly controversial, and they wanted to discourage overuse, so they limited it to only those cases for which a strong motivating use case was put forward, leaving = the convenient tool for all other cases.

There's a lot of things the walrus operator won't do that it could do (assigning to things looked up on a sequence or mapping, assigning to attributes, etc.), but it would encourage using it all the time, to the detriment of the readability of typical code. Sure, you could choose to write more readable code, ignoring the opportunity to use weird and terrible punctuation-filled nonsense, but if my (and many people's) experience with Perl is any guide, people will use shortcuts to get it done faster now even if the resulting code is unreadable, even by them, a month later.

There are other minor hurdles that get in the way (supporting all of the augmented assignment approaches with the walrus would add a ton of new byte codes to the interpreter, expanding the eval loop's switch significantly, and potentially inhibiting optimizations/spilling from CPU cache), but fundamentally, your motivating case of using a list comprehension for side-effects is a misuse of list comprehensions (a functional construct that, like all functional programming tools, is not intended to have side-effects), as are most cases that would rely on augmented assignment expressions. The strong motivations for introducing this feature were things you could reasonably want to do and couldn't without the walrus, e.g.

  1. Regexes, replacing this terrible, verbose arrow pattern:

    m = re.match(r'pattern', string)
    if m:
        do_thing(m)
    else:
        m = re.match(r'anotherpattern', string)
        if m:
            do_another_thing(m)
        else:
            m = re.match(r'athirdpattern', string)
            if m:
                do_a_third_thing(m)
    

    with this clean chain of tests:

    if m := re.match(r'pattern', string):
        do_thing(m)
    elif m := re.match(r'anotherpattern', string):
        do_another_thing(m)
    elif m := re.match(r'athirdpattern', string):
        do_a_third_thing(m)
    
  2. Reading a file by block, replacing:

    while True:
        block = file.read(4096)
        if not block:
            break
    

    with the clean:

    while block := file.read(4096):
    

Those are useful things people really need to do with some frequency, and even the "canonical" versions I posted are often misimplemented in other ways (e.g. applying the regex test twice to avoid the arrow pattern, duplicating the block = file.read(4096) once before loop and once at the end so you can run while block:, but in exchange now continue doesn't work properly, and you risk the size of the block changing in one place but not another); the walrus operator allowed for better code.

The listcomp accumulator isn't (much) better code. itertools.accumulate exists, and even if it didn't, the problem can be worked around in other ways with simple generator functions or hand-rolled loops. The PEP does describe it as a benefit (it's why they allowed walrus assignments to "escape" comprehensions), but the discussion relating to this scoping special case was even more divided than the discussion over adding the walrus itself; you can do it, and it's arguably useful, but it's not something where you look at the two options and immediately say "Man, if not for the walrus, this would be awful".

And just to satisfy the folks who want evidence of this, note that they explicitly blocked some use cases that would have worked for free (it took extra work to make the grammar prohibit these usages), specifically to prevent walrus overuse. For example, you can't do:

x := 1

on a line by itself. There's no technical reason you can't, at all, but they intentionally made it a syntax error for the walrus to be used without being wrapped in something. (x := 1) works at top level, but it's annoying enough no one will ever choose it over x = 1, and that was the goal.

Until someone comes up with a common code pattern for which the lack of :+= makes it infeasibly/unnecessarily ugly (and it would have to be really common and really ugly to justify the punctuation filled monstrosity that is :+=), they won't consider it.

like image 83
ShadowRanger Avatar answered Oct 04 '22 18:10

ShadowRanger


The itertools module provides a lot of the mechanisms to achieve the same result without bloating the language:

partial_sums = list(accumulate(values))
total        = partial_sums[-1] 
like image 21
Alain T. Avatar answered Oct 04 '22 18:10

Alain T.