<p>Generators let you produce a sequence of values over time, without building the whole sequence in memory at once. They're one of Python's most elegant features once they click.</p>
<h2>The Basic Idea</h2>
<p>A generator function looks like a normal function but uses <code>yield</code> instead of <code>return</code>:</p>
<pre><code>def count_up_to(n):
i = 1
while i <= n:
yield i
i += 1
for num in count_up_to(5):
print(num)
# 1 2 3 4 5
</code></pre>
<p>Calling <code>count_up_to(5)</code> doesn't run the function body immediately — it returns a generator object. Code only executes as values are requested, one <code>yield</code> at a time.</p>
<h2>Why Not Just Use a List?</h2>
<p>Consider processing a huge file:</p>
<pre><code>def read_large_file(path):
with open(path) as f:
for line in f:
yield line.strip()
for line in read_large_file("huge_log.txt"):
process(line)
</code></pre>
<p>This never loads the whole file into memory — only one line exists at a time. Compare that to <code>f.readlines()</code>, which builds a full list upfront. For a 10GB log file, that difference is the difference between running fine and crashing with a memory error.</p>
<h2>Generator Expressions</h2>
<p>Just like list comprehensions, but lazy:</p>
<pre><code>squares = (x * x for x in range(1_000_000)) # generator, no memory used yet
total = sum(squares) # values produced one at a time
</code></pre>
<p>Swap the brackets <code>[]</code> for parentheses <code>()</code> and a list comprehension becomes a generator expression.</p>
<h2>State Is Preserved Between Calls</h2>
<p>A generator remembers exactly where it left off, including local variables:</p>
<pre><code>def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
fib = fibonacci()
print(next(fib)) # 0
print(next(fib)) # 1
print(next(fib)) # 1
print(next(fib)) # 2
</code></pre>
<p>This makes generators perfect for infinite or unbounded sequences — something a list could never represent.</p>
<h2>yield from: Delegating to Sub-Generators</h2>
<pre><code>def inner():
yield 1
yield 2
def outer():
yield 0
yield from inner()
yield 3
print(list(outer())) # [0, 1, 2, 3]
</code></pre>
<h2>Sending Values Into a Generator</h2>
<p>Generators can also receive values, turning them into simple coroutines:</p>
<pre><code>def running_total():
total = 0
while True:
value = yield total
total += value
gen = running_total()
next(gen) # prime the generator
print(gen.send(5)) # 5
print(gen.send(3)) # 8
</code></pre>
<h2>Key Takeaway</h2>
<p>Generators trade eagerness for laziness: instead of computing everything up front, they compute one value at a time, exactly when needed. This makes them the natural choice for large datasets, infinite sequences, and streaming pipelines where memory efficiency matters.</p>
Comments (0)
No comments yet
Be the first to share your thoughts!