Use of yield with a dict comprehension

skyork picture skyork · Sep 10, 2012 · Viewed 9k times · Source

As a contrived example:

myset = set(['a', 'b', 'c', 'd'])
mydict = {item: (yield ''.join([item, 's'])) for item in myset}

and list(mydict) gives:

['as', 'cs', 'bs', 'ds', {'a': None, 'b': None, 'c': None, 'd': None}]

What happens here? What does yield do? And is this behavior consistent no matter what expression follows yield?

Note: I know that doing mydict = {item: ''.join([item, 's']) for item in myset} would give the dictionary {'a': 'as', 'b': 'bs', 'c': 'cs', 'd': 'ds'}, which seems to be what I am trying to do here.

Answer

orlp picture orlp · Sep 10, 2012

First of all, what does yield return? The answer in this case is None, because yield returns the parameter passed to next(), which is nothing in this case (list doesn't pass anything to next).

Now here's your answer:

>>> myset = set(['a', 'b', 'c', 'd'])
>>> mydict = {item: (yield ''.join([item, 's'])) for item in myset}
>>> mydict
<generator object <dictcomp> at 0x0222BB20>

The dict comprehension is turned into a generator, because you used yield in a function body context! This means that the whole thing isn't evaluated until it's passed into list.

So here's what happens:

  1. list calls next(mydict).
  2. Yield returns ''.join([item, 's']) to list and freezes the comprehension.
  3. list calls next(mydict).
  4. The comprehension resumes and assigns the result of yield (None) to item in the dictionary and starts a new comprehension iteration.
  5. Go back to 1.

And at last the actual generator object returns the temporary in the body, which was the dict. Why this happens is unknown to me, and it's probably not documented behaviour either.