What surprised me about the Python assignment puzzle

August 21, 2015

Yesterday I wrote about a Python assignment puzzle and how it worked, but I forgot to write about what was surprising about it for me. The original puzzle is:

(a, b) = a[b] = {}, 5

The head-scratching bit for me was the middle, including the whole question of 'how does this even work'. So the real surprise here for me is that in serial assignments, Python processes the assignments left to right.

The reason this was a big surprise is due to what was my broad mental model of serial assignment, which comes from C. In C, assignment is an expression that yields the value assigned (ie the value of 'a = 2' is 2). So in C and languages like this, serial assignment is a series of assignment expressions that happen right to left; you start out with the actual expression producing a value, you do the rightmost assignment which yields the value again, and you ripple leftwards. So a serial assignment groups like this:

a = (b = (c = (d = <expression>)))

Python doesn't work this way, of course; assignment is not an expression and doesn't produce a value. But I was still thinking of serial assignment as proceeding right to left by natural default and was surprised to learn that Python has chosen to do it in the other order. There's nothing wrong with this and it's perfectly sensible; it's just a decision that was exactly opposite from what I had in my mind.

(Looking back, I assumed in this entry that Python's serial assignment order was right to left without bothering to look it up.)

How did my misapprehension linger for so long? Well, partly it's that I don't use serial assignment very much in Python; in fact, I don't think anyone does much of it and I have the vague impression that it's not considered good style. But it's also that it's quite rare for the assignment order to actually matter, so you may not discover a mistaken belief about it for a very long time. This puzzle is a deliberately perverse exercise where it very much does matter, as the leftmost assignment actively sets up the variables that the next assignment then uses.


Comments on this page:

I wasn't aware that Python did serial assignment this way, and I think it's less intuitive than the right-to-left way of doing it. When I package up the assignment in a function and disassemble it, here's what I get:

   >>> dis.dis(f)
     4           0 BUILD_MAP                0
                 3 LOAD_CONST               1 (5)
                 6 BUILD_TUPLE              2
                 9 DUP_TOP
                10 UNPACK_SEQUENCE          2
                13 STORE_FAST               0 (a)
                16 STORE_FAST               1 (b)
                19 LOAD_FAST                0 (a)
                22 LOAD_FAST                1 (b)
                25 STORE_SUBSCR

     5          26 LOAD_FAST                0 (a)
                29 LOAD_FAST                1 (b)
                32 BUILD_TUPLE              2
                35 RETURN_VALUE

So what the statement is actually doing is, first, constructing the value on the far right of the statement, but then assigning it left to right. In other words, conceptually, the execution order is right, left, middle, so it skips around (unlike the right-to-left assignment order, which would do it right, middle, left). Since Python normally tries to make syntax as intuitive as possible, I'm kind of surprised at this, and it makes me wonder if there is some rationale for doing it this way that I'm missing.

By cks at 2015-08-24 10:20:16:

I think that doing assignment right to left is only intuitive if you think of evaluation and assignment as being one thing (as it is in C). If you consider it as two completely separate things, then evaluating the expression and then assigning it left to right is perfectly reasonable. Left to right is how we read and how a lot of things in Python work in general (including many expressions).

(Of course this suggests that the truly natural way to write assignment would be '<expression> = <var>'. I don't think any vaguely common high level languages do this, although some assembly languages put the target register on the right side so you get syntax like 'add src1, src2, dest'.)

I agree left to right assignment is intuitive in itself, but as you note, if you do it that way it makes more sense to put the expression being evaluated on the far left, instead of the far right.

Just two small comments ... First, I think that separating assignments from expressions is a good thing. It avoids the common mistake to type '=' when you actually mean '==' (or vice versa) that often happens in C and C-like languages and can cause subtle bugs. In Python you get a syntax error from the parser.

Second, the way serial assignments work in Python is actually very simple and intuitive. Except if you've been a C programmer before you came to Python. ;-)

First, the expression on the right is evaluated. Then the result is assigned to every variable, from left to right. So, for example, when you write

a = b = c = expr

then expr is evaluated, then the result is assigned to a, then to b, and finally to c. So, the above is the same as:

t = expr
a = t
b = t
c = t

Actually, the order matters only because assignments can have side effects (e.g. if a class overrides the assignment operator, or if the same variable is used more than once). In such a case it's better to avoid serial assignments. If there are no side effects, the order in which the assignments are performed doesn't matter at all.

Actually, all of this is beginner's stuff. Once you have seriously started to learn Python and used it for some time, you don't even have to think about all of that at all.

Best regards
Oliver

Written on 21 August 2015.
« What's going on with a Python assignment puzzle
I think you should get your TLS configuration advice from Mozilla »

Page tools: View Source, View Normal, Add Comment.
Search:
Login: Password:
Atom Syndication: Recent Comments.

Last modified: Fri Aug 21 21:58:55 2015
This dinky wiki is brought to you by the Insane Hackers Guild, Python sub-branch.