[Python-ideas] Inline assignments using "given" clauses

Steven D'Aprano steve at pearwood.info
Sat May 12 14:27:35 EDT 2018


Part 2.

On Sat, May 12, 2018 at 08:16:07AM -0700, Neil Girdhar wrote:
> I love given compared with := mainly because

[...]
> * Python has a reputation for being working pseudocode, and given reads 
> like pseudocode.  := needs to be deciphered by comparison especially in the 
> complicated cases where multiple := operators are used on one line.

Until you learn and become familiar with a new syntax, there is 
generally going to be a period you have to decipher it. I spent a long 
time mentally translating list comprehensions into mathematical set 
builder notation before it became second nature to me.

Even now, I know people who find decorators and comprehensions 
indecipherable. Or at least, so they claim, and they aren't motivated to 
bother learning them. Oh well, that's their loss.

Binding-expressions aren't like asynchronous programming, where the 
entire coding paradigm is different, and you literally have to think 
about your algorithms in another way. Whether you spell the binding 
operation

    target := expr
    expr as target
    expr -> target
    target given target = expr
    let target = expr
    : target expr ;

(that last one is stolen from Forth, and should not be taken as a 
serious suggestion)

is just a matter of spelling. We'll get used to whatever spelling it is. 
Some may be more convenient than others, or more error-prone, or may be 
harder to parse, but they're secondary issues. (Important, but still 
secondary.) Fundamentally, the operation is the same regardless of the 
spelling:

- evaluate an expression
- bind that value to a name
- return the value


(Except of course Nick's "given" suggestion is more complex, since the 
returned value is not necessarily the same as the bound value.)


> * there are no difficult mental questions about evaluation order, e.g., in 
> a bracketed expression having multiple assignments.

Aren't there just?

    x = 1
    print( x + x given x = 50 )


Will that print 100, or 51? Brackets ought to make it clearer:

    (x + x given x = 50)  # this ought to return 100
    x + (x given x = 50)  # this ought to return 51

but if I leave the brackets out, I have no idea what I'll get.



> Similarly, instead of 
> (a.b(a) given a = c.d())  do I write (a.b(a := c.d())) or ((a := 
> c.d()).b(a)) ?


I would expect that your first example is a NameError:

    a.b(a := c.d())

since Python evaluates arguments to a method *after* looking up the 
method. So that corresponds to:

- look up "a"  # NameError, unless you've already got an "a"
- look up "a.b"
- evaluate c.d()
- assign that value to a
- pass that to the a.b method we found earlier


What you probably want is the second version:

    (a := c.d()).b(a)

which of course looks like utter crap. It might look better if we use at 
least half-way sensible variable names and a more realistic looking 
example, instead of obfuscated one-letter names.

    (widget := widget_builder.new(*args)).method(widget)


> * it avoids the question of what happens when := is used in a switch:  (a 
> if (b := c) else d)   Sometimes you want the assignment to happen 
> unconditionally (a if (b:=c) else d) + b; sometimes you don't.  How do you 
> force one case or the other?

I think that this argument is really weak. The obvious answer is, if you 
don't want the assignment to happen unconditionally, then don't do the 
assignment unconditionally. Where you do it depends on when you want the 
assignment to take place. There's no mystery here.


# unconditionally pop from a list
(spam if mylist.pop(idx) else eggs) + mylist

# conditionally pop from a list
(mylist.pop(idx) if condition else eggs) + mylist
(spam if condition else mylist.pop(idx)) + mylist


Take your choice of which you want:

(spam if (mylist := expr) else eggs) + mylist
((mylist := expr) if condition else eggs) + mylist
(spam if condition else (mylist := expr)) + mylist

Of course, in the last two cases, you're going to get a NameError when 
you try to add mylist if the ternary operator took the wrong branch. 
Unless you already defined mylist earlier.

If you want something else, you have to explain what you want.


> given makes it obvious by separating 
> assignment from the usage of its assignment target.

This is just a purely mechanical source transformation from one to the 
other. Just replace ":" with "mylist given mylist " and you are done.

# unconditional version
(spam if (mylist given mylist = expr) else eggs) + mylist

# conditional versions
((mylist given mylist = expr) if condition else eggs) + mylist
(spam if condition else (mylist given mylist = expr)) + mylist

If you can write the "given" versions, then just do the reverse 
transformation and replace the redundant verbosity with a colon.


> Style:
> * it avoids the big style question of when to use and when not to use :=.  
> (Even if you ask people not to, people are going to write the 
> expression-statement a := b as a synonym for the statement a = b.)

What if they do? Is it really the end of the world if some ex-Pascal 
coders or people with an over-developed desire for (foolish) consistency 
add a colon to their statement level assignments?

Some people add semi-colons to their statements too. Do we care? No.

But if it aggitates people so much, then I'm perfectly happy with 
Guido's suggestion that we simply ban top level binding expressions and 
require them to leave the colons out.


> * it looks a lot like the existing Python "for" and "if" clauses, which 
> also do in-expression assignments.

"if" clauses do an assignment? Have you borrowed the keys to Guido's 
time machine, and are writing from 2025 and Python 4.2? *wink*

I don't think "given" expressions look even remotely similar to either.

    for target in iterable:

    if condition:

    another_expr given target = expr


Aside from "all three use a keyword".


-- 
Steve


More information about the Python-ideas mailing list