11

What is the difference between list comprehensions and generator comprehensions with yield inside? Both return a generator object (listcomp and genexpr respectively), but upon full evaluation the latter adds what seem to be rather superfluous Nones.

>>> list([(yield from a) for a in zip("abcde", itertools.cycle("12"))])
['a', '1', 'b', '2', 'c', '1', 'd', '2', 'e', '1']

>>> list(((yield from a) for a in zip("abcde", itertools.cycle("12"))))
['a', '1', None, 'b', '2', None, 'c', '1', None, 'd', '2', None, 'e', '1', None]

How come? What is the scientific explanation?

Mischa Arefiev
  • 5,227
  • 4
  • 26
  • 34
  • 1
    @Alik, @Antti Haapala, please, remove the "duplicate" marking. This question asks about the behavior using a `yield from` Python statement. The linked "duplicate" answer asks a similar question about `yield` Python statement. These two statements are distinct. And since `yield from` was only recently added to the language it is quite natural that there are new unexpected behaviors which it produces. This should result in some questions which, while they may look similar to the questions about `yield`, are not the same questions as the ones about `yield`. – Dmitry Rubanovich Jan 27 '16 at 13:21
  • are you still looking for an answer to your question? – Daniel Feb 29 '16 at 07:05
  • The first case is actually throwing away the result of the list comprehension. The result is from the side-effect of the comprehension, caused by ``yield from a``. – MisterMiyagi May 07 '18 at 12:17

3 Answers3

2

TLDR: A generator expression uses an implicit yield, which returns None from the yield from expression.

There are actually two things behaving differently here. Your list comprehension is actually thrown away...

  • Once again with clarity

Understanding this is easiest if you transform the expressions to equivalent functions. For clarity, let's write this out:

listcomp = [<expr> for a in b]
def listfunc():
    result = []
    for a in b:
        result.append(<expr>)
    return result

gencomp = (<expr> for a in b)
def genfunc():
    for a in b:
        yield <expr>

To replicate the initial expressions, the key is to replace <expr> with (yield from a). This is a simple textual replacement:

def listfunc():
    result = []
    for a in b:
        result.append((yield from a))
    return result

def genfunc():
    for a in b:
        yield (yield from a)

With b = ((1,), (2,)), we would expect the output 1, 2. Indeed, both replicate the output of their respective expression/comprehension forms.

As explained elsewhere, yield (yield from a) should make you suspicious. However, result.append((yield from a)) should make you cringe...

  • Yielding the answer

Let's look at the generator first. Another rewrite makes it obvious what is going on:

def genfunc():
    for a in b:
        result = (yield from a)
        yield result

For this to be valid, result must have a value - namely None. The generator does not yield the (yield from a) expression, but its result. You only get the content of a as a side effect of evaluating the expression.

  • Returning to the question

If you check the type of your "list comprehension", it is not list - it is generator. <listcomp> is just its name. Yes, that's not a Moon, that's a fully functional generator.

Remember how our transformation put a yield from inside a function? Yepp, that is how you define a generator! Here is our function version, this time with print sprinkled on it:

def listfunc():
    result = []
    for a in b:
        result.append((yield from a))
        print(result[-1])
    print(result)
    return result

Evaluating list(listfunc()) prints None, None (from the append), and [None, None] (from the result) and yields 1, 2. Your actual list contains those None that sneaked into the generator as well! However, it is thrown away and the result is again just a side effect. This is what actually happens:

  • A generator is created upon evaluating the list comprehension/listfunc.
  • Feeding it to list iterates over it...
    • yield from a yields the values of a to list and returns None to the comprehension/listfunc
    • None is stored in the result list
  • At the end of the iteration...

    • return raises StopIteration with a value of [None, None]
    • The list constructor ignores this and throws the value away
  • Moral of this story

Don't use yield from inside of comprehensions. It does not do what you think it does.

MisterMiyagi
  • 44,374
  • 10
  • 104
  • 119
  • 2
    `yield` in comprehensions and genexps is going to be [deprecated](https://docs.python.org/3.7/whatsnew/3.7.html#deprecated) in Python 3.7 and forbidden in Python 3.8, because it's so confusing and violates invariants like "list comprehensions evaluate to lists". – user2357112 May 13 '18 at 19:08
1

The value of the yield from expression is None. The fact that your second example is a generator expression means that it is already implicitly yielding from the iterator, so it will also yield the value of the yield from expression. See this for a more detailed answer.

Community
  • 1
  • 1
ml-moron
  • 888
  • 1
  • 11
  • 22
0

Both examples are deprecated in Python 3.8 because of the confusion and throw SyntaxError: 'yield' inside list comprehension. See the buglog for 3.8 for the release notes.

Union find
  • 7,759
  • 13
  • 60
  • 111