Generators: lazy, one-shot, and why they save memory
A function with yield in its body is a generator function; calling it doesn't run the body — it returns a
generator object (an iterator) that runs the body lazily, one yield at a time, on each next(). Compare
memory behavior: a list comprehension builds the entire list up front; a generator expression produces
values on demand and never holds more than one at a time.
def countdown(n):
print(f" (starting countdown from {n})")
while n > 0:
yield n
n -= 1
print(" (countdown done)")
gen = countdown(3)
print("generator created, body has NOT run yet:", gen)
print("first next():", next(gen))
print("second next():", next(gen))
print("third next():", next(gen))
try:
next(gen)
except StopIteration:
print("StopIteration once exhausted")
import sys
list_comp = [x * x for x in range(100_000)]
gen_exp = (x * x for x in range(100_000))
print("list comprehension size:", sys.getsizeof(list_comp), "bytes")
print("generator expression size:", sys.getsizeof(gen_exp), "bytes <- constant, regardless of range size")
print("sum via generator (never materializes the full list):", sum(x * x for x in range(100_000)))