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...
CodePudding user response:
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.
CodePudding user response:
@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