4

I just recently came across this cool hack in Python.

This:

d = {}
for i, d[i] in enumerate('abc'):
    pass

>>> d
{0: 'a', 1: 'b', 2: 'c'}
>>> 

This assigns key value pairs to a empty dictionary from the iterator.

I would like to know how Cython backend parses this, my expectation is that it's being parsed with unpacking assignment. But it would be nice to know the actual Cython implementation of this, and also if doing this is recommended or not?

I know I just can simply do:

d = {}
for i, v in enumerate('abc'):
    d[i] = v

But the cool hack above can do this with shorter code, but I am not sure if it is considered good practice in Python.

I never seen anybody use this...

U13-Forward
  • 69,221
  • 14
  • 89
  • 114
  • 3
    speaking of shorter code `d = dict(enumerate('abc'))` is much better. But I respect the curiosity of how it is done with respect to your code snippet. – buran Sep 17 '21 at 08:10
  • Oh, snap. I didn't even check. Because that is just so obvious :D – user2390182 Sep 17 '21 at 08:12
  • Actually the first version is longer than the second one :D – Szabolcs Sep 17 '21 at 08:18
  • 1
    You mean in *cpython*? – Abdul Niyas P M Sep 17 '21 at 08:20
  • @buran In this particular usage, yes. In other cases it has some benefits, as I described in my answer now. – no comment Sep 19 '21 at 15:36
  • @Szabolcs No, [first version is shorter](https://tio.run/##K6gsycjPM7YoKPr/v6AoM69EIyc1T0NdXZ0rRcFWobqWKy2/SCFTRyElOjNWITNPITWvNDe1KLEkVUM9MSlZXdOKSwEIChKLi7mAmjQ1ubjwmVKG2wiwBbYKZRBj/v8HAA), even when the variable is just a single letter. And the difference increases by 2 for every extra character in the variable. (And if you're golfing, you could replace `pass` with `0`). – no comment Sep 19 '21 at 15:39
  • @buran Yes, of course I know. Well I was just showing a less "hacky" demonstration. – U13-Forward Sep 20 '21 at 02:29
  • @don'ttalkjustcode For sure it is shorter, but I didn't know you mean faster by saying shorter – Szabolcs Sep 20 '21 at 11:11
  • @Szabolcs Huh? I didn't. Now I really don't know what *you* are saying. – no comment Sep 20 '21 at 11:18
  • Now I see you meant shorter in the aspect of dis, I was talking about shorter length. – Szabolcs Sep 20 '21 at 11:22
  • @Szabolcs What do you mean with "shorter length", and which one do you think now is shorter in that regard? I clearly meant source code size there, since I said it with a link demonstrating exactly that and talked about the length of the variable name. – no comment Sep 20 '21 at 11:52

2 Answers2

2

You don't have to read CPython code since the behavior is defined in the Python documentation already.

If you read the documentation of the for statement, the target list in a for statement uses rules of a standard assignment:

Each item in turn is assigned to the target list using the standard rules for assignments (see Assignment statements)

And if you read the rules for assignment statements, you can see that each item in the target list of an assignment is assigned to in a left-to-right order:

An assignment statement evaluates the expression list (remember that this can be a single expression or a comma-separated list, the latter yielding a tuple) and assigns the single resulting object to each of the target lists, from left to right.

So in the first iteration of your for loop, where a tuple 0, 'a' is generated:

for i, d[i] in enumerate('abc')

An assignment statement equivalent to the following is executed:

i, d[i] = 0, 'a'

which assigns 0 to i first since it's on the left, and then 'a' to d[i], which evaluates to d[0], effectively making d[0] = 'a'.

The same goes for the rest of the iterations.

blhsing
  • 91,368
  • 6
  • 71
  • 106
-3

@blhsing already explained that it's ordinary left-to-right assignment. I've sometimes done this and similar variations, and I'd like to add why:

  • Brevity. It's slightly shorter, even when your extra variable is only one letter.
  • Speed. It avoids pointless storing and loading of a variable.
  • Cleanliness. Doesn't pollute the namespace with a pointless variable.
  • Laziness. Don't want to have to think of a variable name. That's hard.
  • Education. I like pointing out things that people aren't familiar with :-)
  • Entertainment. Related to the previous point - I like to puzzle people :-D

So to answer your "recommended or not?" and whether it's "considered good practice": I'd say yes, I think it's perfectly fine and even has advantages. The only potential downside I see is that some people might complain about it purely because they're not familiar with it and don't like unfamiliar things.

About the speed aspect: As shown with dis.dis below, everything's the same for both versions except for the additional STORE and LOAD of the extra variable v.

dis.dis('''                       |  dis.dis('''
d = {}                            |  d = {}
for i, d[i] in enumerate('abc'):  |  for i, v in enumerate('abc'):
    pass                          |      d[i] = v
''')                              |  ''')
----------------------------------+-----------------------------------
 0 BUILD_MAP        0             |   0 BUILD_MAP        0               
 2 STORE_NAME       0 (d)         |   2 STORE_NAME       0 (d)           
 4 LOAD_NAME        1 (enumerate) |   4 LOAD_NAME        1 (enumerate)   
 6 LOAD_CONST       0 ('abc')     |   6 LOAD_CONST       0 ('abc')       
 8 CALL_FUNCTION    1             |   8 CALL_FUNCTION    1               
10 GET_ITER                       |  10 GET_ITER                         
12 FOR_ITER        12 (to 26)     |  12 FOR_ITER        16 (to 30)       
14 UNPACK_SEQUENCE  2             |  14 UNPACK_SEQUENCE  2               
16 STORE_NAME       2 (i)         |  16 STORE_NAME       2 (i)      
                                  |  18 STORE_NAME       3 (v)
                                  |  20 LOAD_NAME        3 (v)
18 LOAD_NAME        0 (d)         |  22 LOAD_NAME        0 (d)           
20 LOAD_NAME        2 (i)         |  24 LOAD_NAME        2 (i)           
22 STORE_SUBSCR                   |  26 STORE_SUBSCR                     
24 JUMP_ABSOLUTE   12             |  28 JUMP_ABSOLUTE   12              
26 LOAD_CONST       1 (None)      |  30 LOAD_CONST       1 (None)       
28 RETURN_VALUE                   |  32 RETURN_VALUE
no comment
  • 6,381
  • 4
  • 12
  • 30
  • Wow! Thanks for your clear input! This was kind of what I expected to, the regular one everyone is familiar with has to store the assign the iterator `v` to the value, then assign it to the dictionary, whereas the one everyone is unfamiliar with doesn't need that extra step, it directly stores it to the dictionary, and doesn't need to store the extra iterator `v`. Yeap! I also like to surprise others with things that people aren't familiar with xd, same as you. – U13-Forward Sep 20 '21 at 02:37
  • Well this deserves some more upvotes, it's very clear :) blhsing provides very good documentation references and explanation as well. And he posted first so I will keep the accept on his :) – U13-Forward Sep 20 '21 at 02:38
  • @U12-Forward Well, I did get another *vote* now. I guess from someone who didn't like how accurately I described them in my "only potential downside" sentence :-) – no comment Sep 20 '21 at 13:30
  • @U12-Forward Yes, that was rather clear (from your comments and the early upvote, presumably from you). I do think it's from someone who either doesn't like such loop assignments (although they didn't downvote the question) or don't like how much I'm advocating them. – no comment Sep 20 '21 at 13:44
  • Uh... That's strange – U13-Forward Sep 21 '21 at 10:19