6

It appears to me that it is not that straight forward to interchange classic while loops with assignment-expressions-loops keeping the code looking great.

Consider example1:

>>> a = 0
>>> while (a := a+1) < 10:
...     print(a)
... 
1
2
3
4
5
6
7
8
9

and example2:

>>> a = 0
>>> while a < 10:
...     print(a)
...     a += 1
... 
0
1
2
3
4
5
6
7
8
9

How would you modify example1 in order to have the same output (not skipping the 0) of example2? (without changing a = 0, of course)

Martijn Pieters
  • 889,049
  • 245
  • 3,507
  • 2,997
alec_djinn
  • 7,288
  • 8
  • 29
  • 58
  • 4
    I am not sure this is a good use case for assignment expressions: `The danger of toy examples is twofold: they are often too abstract to make anyone go "ooh, that's compelling", and they are easily refuted with "I would never write it that way anyway".` https://www.python.org/dev/peps/pep-0572/#rationale – Scott Skiles Mar 22 '19 at 18:27
  • 1
    Changing `a` would also be my choice. I kinda don't like it though. There must be a better way! – alec_djinn Mar 22 '19 at 18:45
  • 4
    I do not see why you would want it to be another way than it is. A while-loop evaluates the code in the loop condition before each iteration. So a statement that increments the variable is run before the first iteration and you need either to start with ``a=-1`` or print ``a-1``. It looks like you would like to have a do-while loop instead of a while loop for what you're doing. – allo Mar 25 '19 at 13:17
  • 1
    @allo I just wanted to test out some use-cases for the new assignment expression. Reading PEP 572 I understood that you while loops are good places to start and I now want to know if there is a better way to deal with the posted example. Changing the variable before the loop is not my favorite solution. So, is there another option or not? – alec_djinn Mar 25 '19 at 13:24
  • 1
    I do not know, I guess it is not designed the way you want to use it, but I am curious for other answers as well. – allo Mar 25 '19 at 13:25
  • `print(a-1)` will work if you don't have to assign `a=-1` – Arihant Mar 25 '19 at 16:12
  • @Arihant It will not. It will print the `0` but not the final `9` – alec_djinn Mar 26 '19 at 08:36
  • IMHO your example is a non-sense since, as I understood assignement-expression, the goal is to avoid pre-assignation of a variable before looping. In your example you actually pre-assign `a` anyway. Your problem is the same we can have in other languages between `a++` and `++a`. The first case matches a `++a` behaviour, the second matches a `a++` behaviour. Your question is equivalent to asking how to code a `++a` behaviour using `a++`; non-sense again. – Tryph Mar 26 '19 at 10:58
  • 2
    The biggest problem I see is that you are simply re-inventing a `for` loop here. There are use-cases for assignment expressions, this just *isn't one of them*. – Martijn Pieters Mar 26 '19 at 22:24
  • @MartijnPieters that could be the final answer. I was just wondering whether or not there is way around here. I would accept NO as an answer. – alec_djinn Mar 27 '19 at 07:37
  • Agree on the criticisms on whether it is a sensible example. For such case, we already have `for a in range(0, 10):` – Adrian Shum Mar 27 '19 at 08:21
  • 1
    This feature is meant for `compute; evaluate; use` sequences while the OP example if `evaluate; use; recompute`. Thus it does not map natively. Use another language construct! – Dima Tisnek Apr 01 '19 at 01:05

3 Answers3

18

Simple loops like your example should not be using assignment expressions. The PEP has a Style guide recommendations section that you should heed:

  1. If either assignment statements or assignment expressions can be used, prefer statements; they are a clear declaration of intent.
  2. If using assignment expressions would lead to ambiguity about execution order, restructure it to use statements instead.

Simple loops should be implemented using iterables and for, they are much more clearly intended to loop until the iterator is done. For your example, the iterable of choice would be range():

for a in range(10):
    # ...

which is far cleaner and concise and readable than, say

a = -1
while (a := a + 1) < 10:
    # ...

The above requires extra scrutiny to figure out that in the loop a will start at 0, not at -1.

The bottom line is that you should not be tempted to 'find ways to use assignment statements'. Use an assignment statement only if it makes code simpler, not more complex. There is no good way to make your while loop simpler than a for loop here.

Your attempts at rewriting a simple loop are also echoed in the Tim Peters's findings appendix, which quotes Tim Peters on the subject of style and assignment expressions. Tim Peters is the author of the Zen of Python (among many other great contributions to Python and software engineering as a whole), so his words should carry some extra weight:

In other cases, combining related logic made it harder to understand, such as rewriting:

while True:
    old = total
    total += term
    if old == total:
        return total
    term *= mx2 / (i*(i+1))
    i += 2

as the briefer:

while total != (total := total + term):
    term *= mx2 / (i*(i+1))
    i += 2
return total

The while test there is too subtle, crucially relying on strict left-to-right evaluation in a non-short-circuiting or method-chaining context. My brain isn't wired that way.

Bold emphasis mine.

A much better use-case for assignment expressions is the assigment-then-test pattern, especially when multiple tests need to take place that try out successive objects. Tim's essay quotes an example given by Kirill Balunov, from the standard library, which actually benefits from the new syntax. The copy.copy() function has to find a suitable hook method to create a copy of a custom object:

reductor = dispatch_table.get(cls)
if reductor:
    rv = reductor(x)
else:
    reductor = getattr(x, "__reduce_ex__", None)
    if reductor:
        rv = reductor(4)
    else:
        reductor = getattr(x, "__reduce__", None)
        if reductor:
            rv = reductor()
        else:
            raise Error("un(shallow)copyable object of type %s" % cls)

The indentation here is the result of nested if statements because Python doesn't give us a nicer syntax to test different options until one is found, and at the same time assigns the selected option to a variable (you can't cleanly use a loop here as not all tests are for attribute names).

But an assignment expression lets you use a flat if / elif / else structure:

if reductor := dispatch_table.get(cls):
    rv = reductor(x)
elif reductor := getattr(x, "__reduce_ex__", None):
    rv = reductor(4)
elif reductor := getattr(x, "__reduce__", None):
    rv = reductor()
else:
    raise Error("un(shallow)copyable object of type %s" % cls)

Those 8 lines are a lot cleaner and easier to follow (in my mind) than the current 13.

Another often-cited good use-case is the if there is a matching object after filtering, do something with that object, which currently requires a next() function with a generator expression, a default fallback value, and an if test:

found = next((ob for ob in iterable if ob.some_test(arg)), None)
if found is not None:
    # do something with 'found'

which you can clean up a lot with the any() function

if any((found := ob).some_test(arg) for ob in iterable):
    # do something with 'found'
Martijn Pieters
  • 889,049
  • 245
  • 3,507
  • 2,997
1

The problem with the question is, the entire approach to the problem appears to be coming from a programmer trying to use Python like other languages.

An experienced Python programmer wouldn't use a while loop in that case. They'd either do this instead:

from itertools import takewhile, count

for a in takewhile(lambda x: x<10, count()):
    print (a)

...or even simpler:

for a in range(10):
    print (a)

As is so often the case (not always of course), the ugly code presented in the question is a symptom of using the language in a less than optimal way.

Rick supports Monica
  • 33,838
  • 9
  • 54
  • 100
  • 1
    I have made just a toy example to illustrate the concept. Of course, I would go with a for loop in this case. My question is more theoretical. "Is there a way?" The answer could also be, "NO, because of this and that ..." – alec_djinn Mar 27 '19 at 07:49
  • But the question only becomes a valid question if you can find an example that actually illustrates what you're trying to ask. Example that you have provided doesn't illustrate it. Find another one. – Rick supports Monica Mar 27 '19 at 12:03
0

I would suggest do-while loop, but it is not supported in Python. Although, you can emulate a while acting as do-while. In such a way you can use the assignment expression

a=0
while True:
    print(a)
    if not((a:=a+1)<10):
        break
Jacob Fuchs
  • 359
  • 1
  • 3
  • 12