Chapter 5 of 6

Comprehensions & Generators

List Comprehensions

Syntax, Multiple for Clauses, Nesting

List Comprehension --- Syntax and Semantics

A list comprehension builds a new list by applying an expression to each element of an iterable, optionally filtering with a condition:

\[\texttt{[}\ \underbrace{\textit{expr}}_{\text{output}} \ \texttt{for}\ \underbrace{x}_{\text{variable}} \ \texttt{in}\ \underbrace{\textit{iterable}}_{\text{source}} \ \underbrace{\texttt{if}\ \textit{condition}}_{\text{optional filter}} \ \texttt{]}\]

Equivalent for loop: result = []\ \ for x in iterable:\ \ \ \ if condition: result.append(expr)

Multiple for clauses: evaluated left-to-right like nested for loops: \[\texttt{[f(x,y)\ for\ x\ in\ A\ for\ y\ in\ B]} \;\equiv\; \texttt{for x in A: for y in B: append(f(x,y))}\]

Produces a new list, evaluated eagerly (immediately).

diagram
List Comprehension Patterns
  • Simple transform: [f(x) for x in it]

  • Filtered transform: [f(x) for x in it if p(x)]

  • Conditional value: [a if cond else b for x in it] (ternary in expression position, not filter)

  • Cartesian product: [(x,y) for x in A for y in B] — outer loop first

  • Flatten 2D: [x for row in matrix for x in row]

  • Nested list comp: [[f(x) for x in row] for row in matrix] — creates 2D result; outer loop is the outer for

  • Walrus in filter: [y for x in it if (y:=f(x)) > 0] — compute once, use in both filter and output

Example 135: List Comprehensions --- All Patterns
# 1. Basic transform
squares = [x**2 for x in range(1, 6)]
print(squares)      # [1, 4, 9, 16, 25]

# 2. Filter
evens = [x for x in range(20) if x % 2 == 0]
print(evens)        # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

# 3. Conditional expression (ternary in output, NOT a filter)
clipped = [x if x > 0 else 0 for x in [-3,-1,0,2,5,-4,8]]
print(clipped)      # [0, 0, 0, 2, 5, 0, 8]

# 4. Cartesian product: both for clauses
pairs = [(x,y) for x in [1,2,3] for y in ['a','b']]
print(pairs)
# [(1,'a'),(1,'b'),(2,'a'),(2,'b'),(3,'a'),(3,'b')]

# 5. Flatten 2D matrix
matrix = [[1,2,3],[4,5,6],[7,8,9]]
flat   = [x for row in matrix for x in row]
print(flat)         # [1, 2, 3, 4, 5, 6, 7, 8, 9]

# 6. Multiple conditions
nums = [x for x in range(100) if x % 3 == 0 if x % 5 == 0]
print(nums)         # [0, 15, 30, 45, 60, 75, 90]

# 7. Nested list comprehension (2D result)
transposed = [[row[i] for row in matrix] for i in range(3)]
print(transposed)   # [[1,4,7],[2,5,8],[3,6,9]]

# 8. Walrus operator: avoid double computation
import math
results = [root for x in range(20) if (root := math.sqrt(x)) > 3.5]
print(results[:4])  # [3.872..., 4.0, 4.123..., ...]

# 9. String processing
words  = ["hello", "world", "python", "gate", "exam"]
titled = [w.title() for w in words if len(w) > 4]
print(titled)       # ['Hello', 'World', 'Python']

# 10. Dict/set from comprehension intermediary
matrix2 = [[1,0,3],[0,5,0],[7,0,9]]
non_zero = [(i,j) for i,row in enumerate(matrix2)
                  for j,v  in enumerate(row) if v != 0]
print(non_zero)    # [(0,0),(0,2),(1,1),(2,0),(2,2)]

Dictionary Comprehensions

Key-Value Mapping Expressions

Dictionary Comprehension --- Syntax

\[\texttt{\{}\ \underbrace{k\texttt{:}v}_{\text{key:value}} \ \texttt{for}\ \langle\textit{vars}\rangle \ \texttt{in}\ \langle\textit{iterable}\rangle \ [\texttt{if}\ \langle\textit{condition}\rangle] \ \texttt{\}}\]

Produces a new dict; if the same key appears more than once, the last value wins (later iteration overwrites earlier).

Example 136: Dictionary Comprehensions --- Transform, Filter, Invert
# 1. Transform values
nums = {"a": 1, "b": 2, "c": 3}
squared = {k: v**2 for k, v in nums.items()}
print(squared)       # {'a': 1, 'b': 4, 'c': 9}

# 2. Filter entries
passing = {k: v for k, v in nums.items() if v > 1}
print(passing)       # {'b': 2, 'c': 3}

# 3. Invert: value -> key (assumes unique values)
orig = {"a": 1, "b": 2, "c": 3}
inv  = {v: k for k, v in orig.items()}
print(inv)           # {1: 'a', 2: 'b', 3: 'c'}

# 4. From two lists (zip)
keys   = ["name", "age", "city"]
values = ["Alice",  25,  "Delhi"]
d = {k: v for k, v in zip(keys, values)}
print(d)   # {'name': 'Alice', 'age': 25, 'city': 'Delhi'}

# 5. Character frequency
text = "abracadabra"
freq = {ch: text.count(ch) for ch in set(text)}
print(freq)   # {'r':2,'c':1,'a':5,'d':1,'b':2}

# 6. Conditional key transformation
words = ["hello", "WORLD", "Python", "GATE"]
normalised = {w.lower(): len(w) for w in words}
print(normalised)  # {'hello':5,'world':5,'python':6,'gate':4}

# 7. Nested dict comprehension
matrix = [[1,2,3],[4,5,6],[7,8,9]]
indexed = {(i,j): matrix[i][j]
           for i in range(3) for j in range(3)}
print(indexed[(1,2)])   # 6

# 8. Duplicate key: last wins
data = [("a",1),("b",2),("a",3),("b",4)]
d2 = {k: v for k, v in data}
print(d2)   # {'a': 3, 'b': 4}  (last 'a' and 'b' values win)

Set Comprehensions

Unique-Element Expressions

Set Comprehension --- Syntax

\[\texttt{\{}\ \langle\textit{expr}\rangle \ \texttt{for}\ \langle\textit{var}\rangle \ \texttt{in}\ \langle\textit{iterable}\rangle \ [\texttt{if}\ \langle\textit{condition}\rangle] \ \texttt{\}}\]

Automatically deduplicates: equivalent to set([...\ comprehension\ ...]). Unordered: iteration order is not guaranteed. Note: {expr} without for is a set literal; empty set must be set(), not {}.

Example 137: Set Comprehensions
# 1. Unique squares
sq = {x**2 for x in range(-4, 5)}
print(sorted(sq))    # [0, 1, 4, 9, 16]  (deduped: (-2)^2 == 2^2)

# 2. Unique first letters
words = ["apple","ant","banana","blueberry","avocado"]
initials = {w[0] for w in words}
print(initials)      # {'a', 'b'}

# 3. Common elements (set comprehension on two lists)
a = [1,2,3,4,5,3,2]
b = [3,4,5,6,7,4,3]
common = {x for x in a if x in b}
print(common)        # {3, 4, 5}

# 4. Characters appearing in both strings
s1, s2 = "abcde", "cdefg"
shared = {c for c in s1 if c in s2}
print(shared)        # {'c', 'd', 'e'}

# 5. Deduplication with transformation
data = [3, -3, 1, -1, 2, -2, 1]
unique_abs = {abs(x) for x in data}
print(unique_abs)    # {1, 2, 3}

Generator Expressions

Lazy Iterators from Expressions

Generator Expression --- Syntax and Laziness

A generator expression has the same syntax as a list comprehension but uses parentheses (or no delimiters when the sole argument to a function):

\[\texttt{(}\ \langle\textit{expr}\rangle\ \texttt{for}\ x\ \texttt{in} \ \langle\textit{iterable}\rangle\ [\texttt{if}\ \langle\textit{cond}\rangle] \ \texttt{)}\]

Key differences from list comprehension:

diagram
diagram
Example 138: Generator Expressions --- Memory, Pipeline, Exhaustion
import sys

# Memory: generator vs list comprehension
lst = [x**2 for x in range(1000)]
gen = (x**2 for x in range(1000))
print(sys.getsizeof(lst))   # ~8056 bytes
print(sys.getsizeof(gen))   # ~104 bytes (just the generator object)

# Generator as argument (no extra parens needed)
total = sum(x**2 for x in range(1001))    # sum of squares 0..1000
print(total)    # 333833500

any_neg = any(x < 0 for x in [1, 2, -3, 4])
print(any_neg)  # True  (short-circuits at -3!)

all_pos = all(x > 0 for x in [1, 2, 3, 4])
print(all_pos)  # True

# Lazy pipeline: large file simulation
def read_lines():
    for i in range(1, 6):
        yield f"line {i}: data={i*10}"

lines   = (line for line in read_lines())         # lazy source
numbers = (int(line.split("=")[1]) for line in lines)  # lazy parse
big     = (n for n in numbers if n > 20)               # lazy filter
result  = list(big)                                    # materialise
print(result)   # [30, 40, 50]

# Exhaustion: generator can only be iterated ONCE
g = (x for x in range(3))
print(list(g))   # [0, 1, 2]
print(list(g))   # []  EMPTY -- generator exhausted!

# Compare with list (reusable)
lst2 = [x for x in range(3)]
print(list(lst2))   # [0, 1, 2]
print(list(lst2))   # [0, 1, 2]  still works!

# next() on generator
g2 = (x**2 for x in range(5))
print(next(g2))   # 0
print(next(g2))   # 1
print(next(g2))   # 4
print(list(g2))   # [9, 16]  rest consumed

Generator Functions

yield Statement

Suspending and Resuming Execution

yield --- Suspending a Function

Any function containing a yield statement becomes a generator function. Calling it returns a generator object (an iterator) without executing any of the function body.

Execution model:

  1. Calling gen_func() returns a generator object; body not run yet

  2. next(gen) runs until the next yield, suspends, and returns the yielded value

  3. next(gen) again resumes from after the yield

  4. When the function returns (or falls off the end), a StopIteration is raised; return value sets StopIteration.value

State preserved between calls: local variables, loop counters, execution pointer — the frame is kept alive, not torn down.

diagram
Example 139: Generator Functions --- Basics and Infinite Streams
# Basic generator
def gen123():
    print("start")
    yield 1
    print("after 1")
    yield 2
    print("after 2")
    yield 3
    print("done")

g = gen123()
print(type(g))       # <class 'generator'>
print(next(g))       # start / 1
print(next(g))       # after 1 / 2
print(next(g))       # after 2 / 3
try:
    next(g)          # done / StopIteration
except StopIteration:
    print("exhausted")

# for loop consumes automatically
for v in gen123():
    print(v)         # start / 1 / after 1 / 2 / after 2 / 3 / done

# return in generator: sets StopIteration.value
def gen_with_return():
    yield 1
    yield 2
    return "final"   # StopIteration.value = "final"

g2 = gen_with_return()
print(next(g2))      # 1
print(next(g2))      # 2
try:
    next(g2)
except StopIteration as e:
    print(e.value)   # final

# Infinite generator (only possible with generator; not with list!)
def natural_numbers(start=1):
    n = start
    while True:
        yield n
        n += 1

gen = natural_numbers()
print([next(gen) for _ in range(7)])   # [1, 2, 3, 4, 5, 6, 7]

# Fibonacci generator
def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

fib = fibonacci()
print([next(fib) for _ in range(10)])
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

# Practical: large file reading without loading all into memory
def read_chunks(text, size=5):
    for i in range(0, len(text), size):
        yield text[i:i+size]

for chunk in read_chunks("Hello World Python GATE", size=5):
    print(repr(chunk), end=" ")
# 'Hello' ' Worl' 'd Pyt' 'hon G' 'ATE'