5

I have been baffled by an odd behaviour of Python locals().
Basically I want to get an item from the dictionary of locals() in a dictionary comprehension, yet it fails. It is an extremely basic thing, so:

>>> foo=123
>>> bar=345
>>> baz=678
>>> {k: locals()[k] for k in ('foo','bar','baz')}
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 1, in <dictcomp>
KeyError: 'foo'
>>> locals()['foo']
123
>>> locale=locals()
>>> {k: locale[k] for k in ('foo','bar','baz')}
{'foo': 123, 'bar': 345, 'baz': 678}
>>> type(locals())
<class 'dict'>
>>> def fun():
...     return {'foo': 123,'bar':345}
... 
>>> {k: fun()[k] for k in ('foo','bar')}
{'foo': 123, 'bar': 345}

On a practical side the uglier {'foo':foo, 'bar': bar} etc. in dict or in string .format() works fine.
It is just that I am missing something so knowing why will increase my coding chi (as of now I don't glow when coding).

Dimitris Fasarakis Hilliard
  • 150,925
  • 31
  • 268
  • 253
Matteo Ferla
  • 2,128
  • 1
  • 16
  • 27
  • 1
    I'd say `{k: locals()[k] for k in ('foo','bar','baz')}` is _much_ uglier than `{'foo': foo, 'bar': bar, ...}` – cs95 Oct 21 '17 at 11:32
  • 3
    A dictcomp introduces its own scope so its `locals()` don't include your variables... – Jon Clements Oct 21 '17 at 11:33
  • @coldspeed, sorry. I suppose it is a matter of semantics of what is ugly —which is curious. Under the scheme lengthiness=ugly and in the case of under three variables, the locals() way is definitely uglier. Under the scheme of illegibility/looks-like-Perl, the locals() way is always uglier. But under the scheme, smart/lower-prob-of-typo the locals() way is a winner. – Matteo Ferla Oct 21 '17 at 11:42
  • Having said that, I just looked at my screen I have: `return json.dumps({'data': {'raw': raw, 'nt': Q.codon_peak_freq, 'AAemp': Q.empirical_AA_probabilities,'AAscheme': Q.scheme_AA_probabilities, 'Qpool': Q.Qpool}, 'html': html})`, which is fine, except that I change some key names, which is indubitably confusing, thus ugly. (Facepalm) – Matteo Ferla Oct 21 '17 at 11:50
  • 1
    @Matteo in your example, writing: `dict(foo=123, bar=456, baz=789)` would also be an option – Jon Clements Oct 21 '17 at 11:54
  • Yes, but that only makes sense in easy cases. In the snippet in my last comment the JS side has stuff like `reply['data']['raw'][mi][base]` (as I inexplicably did not write a `var data = reply['data']` —over-veneration of the principle of avoidance of "global namespace pollution"?) which is illegible and is a case against keeping stuff overly structured. – Matteo Ferla Oct 21 '17 at 12:21

1 Answers1

8

Because all comprehensions in Python 3 are implemented using a hidden function, calling locals doesn't return the values you're expecting it to return.

You can see this by printing the values out:

>>> _ = {k: print(locals()) for k in ('foo','bar','baz')}
{'k': 'foo', '.0': <tuple_iterator object at 0x7fdf840afa90>}
{'k': 'bar', '.0': <tuple_iterator object at 0x7fdf840afa90>}
{'k': 'baz', '.0': <tuple_iterator object at 0x7fdf840afa90>}

Assigning locals() to locale, as you do, takes care of this. You aren't calling locals inside the comprehension.


Note that, in Python 2, the situation is a bit murkier. dict-comps do fail in a similar fashion but list-comps, which predate dict-comps, work just fine:

>>> _ = [locals()[k] for k in ('foo', 'bar', 'baz')]
>>> _
[20, 40, 60]

this is yet another 'wart' that was addressed with Py3.

Dimitris Fasarakis Hilliard
  • 150,925
  • 31
  • 268
  • 253