Python · comprehensions

Generator Expressions

A comprehension in round brackets: it describes the values, and computes each one only when it is asked for.

Basic generator expression


          # generator expression (lazy)
          gen = (x**2 for x in range(5))
          # ans: <generator object>
          list(gen)
          # ans: [0, 1, 4, 9, 16]
        

Making gen computes nothing. list() pulls the values through one by one, squaring each as it passes.

Generator vs list


        # list - all in memory
        lst = [x**2 for x in range(1000)]
        # generator - on demand
        gen = (x**2 for x in range(1000))
      

List: all 1000 squares are stored

computed up front, held in memory

Generator: one at a time

0

x = 0: only the current value exists

Iterating a generator


          # iterate once
          gen = (x for x in range(3))
          for val in gen:
              print(val)
          # ans: 0, 1, 2
          # ans: exhausted after iteration
          for val in gen:
              print(val)
          # ans: (nothing)
        

Generators inside function calls


        # use in sum
        sum(x**2 for x in range(10))
        # ans: 285
        # use in max
        max(x for x in range(10) if x % 2 == 0)
        # ans: 8
      

When the generator is the only argument, its own brackets can be dropped. No list is ever built.

Generator with a condition


          # filter with generator
          gen = (x for x in range(10) if x % 2 == 0)
          list(gen)
          # ans: [0, 2, 4, 6, 8]
        

Odd values are checked and skipped (red); only even ones come out.

Exercises

Predict the answer, then press → to check.

Exercise 1


        # generator type?
        gen = (x for x in range(3))
        type(gen)
        # ans: <class 'generator'>
      

Exercise 2


          # can iterate twice?
          gen = (x for x in range(3))
          list(gen)
          # ans: [0, 1, 2]
          list(gen)
          # ans: [] (exhausted)
        

Exercise 3


        # memory efficiency?
        import sys
        lst = [x for x in range(1000)]
        gen = (x for x in range(1000))
        sys.getsizeof(lst)
        # ans: ~9000 bytes
        sys.getsizeof(gen)
        # ans: ~200 bytes
      
lst
~9000 bytes
gen
~200 bytes

Exercise 4


          # next() on generator?
          gen = (x for x in range(3))
          next(gen)
          # ans: 0
          next(gen)
          # ans: 1
        

Exercise 5


          # generator with if-else?
          gen = (x if x > 0 else 0 for x in [-1, 1, 2])
          list(gen)
          # ans: [0, 1, 2]
        

An if-else before for transforms every value; nothing is filtered out.

Exercise 6


          # chaining generators?
          gen1 = (x for x in range(5))
          gen2 = (x*2 for x in gen1)
          list(gen2)
          # ans: [0, 2, 4, 6, 8]
        

Each value is pulled from gen1 and doubled by gen2 on its way out.

Exercise 7


        # generator in sum?
        sum(x for x in range(101))
        # ans: 5050
      

Exercise 8


        # empty generator?
        gen = (x for x in [])
        list(gen)
        # ans: []
      

Exercise 9


        # generator vs range?
        range(10)  # range object
        (x for x in range(10))  # generator
        # ans: both are lazy
      

But a range can be iterated again and indexed; a generator is used up after one pass.

Exercise 10


          # convert to list?
          gen = (x**2 for x in range(3))
          lst = list(gen)
          # ans: [0, 1, 4]
          # ans: gen is now exhausted
        

Remember

Round brackets

(expr for x in it if cond) makes a generator, not a tuple.

Lazy

Values are computed when asked for, so memory stays tiny.

One pass

After it is exhausted, it gives nothing more.

Feed functions

sum, max, list and friends consume generators directly.