Write a Python generator function and show that it produces values lazily.
A generator is a function that produces a sequence of values on demand instead of building them all up front. You write it with yield in place of return.
def counter(limit):
for i in range(limit):
yield i * 10
gen = counter(4)
print(type(gen).__name__) # generator
print(next(gen)) # 0
print(next(gen)) # 10
print(list(gen)) # [20, 30] - only what was leftCalling counter does not run the body. It returns a generator object, and the body only starts when you ask for the first value. Each call to next runs the body up to the next yield, produces one value and suspends the function there, keeping its local variables alive.
The list call at the end shows the laziness clearly: it only produced the two values that had not been yielded yet. Once a generator is exhausted, calling next on it again raises StopIteration, and list on it gives an empty list.
Two details worth knowing. The generator remembers where it was, so resuming continues from the yield rather than restarting the function. And the local variables of the suspended frame are preserved, which is what makes the generator useful for loops over data that is being produced as you go.
A quick way to see the memory difference is to compare sys.getsizeof on a list of a million values with the same values produced by a generator expression. The list is tens of megabytes; the generator object is a couple of hundred bytes.