Iterators & Generators
Generator send() and Coroutines
Sometimes generators need input while running - like a parser that needs the next chunk of data, or a state machine that receives commands. The send() method enables two-way communication, turning generators into simple coroutines.
close() and throw()
basic.py
# Basic send()
def echo():
"""Generator that echoes received values"""
while True:
value = yield # receive value
print(f"Echo: {value}")
# Create and prime generator
gen = echo()
next(gen) # prime it (run until first yield)
# Send values
message =
gen.send(message)
gen.send(42)
gen.send([1, 2, 3])
# Basic send()
def echo():
"""Generator that echoes received values"""
while True:
value = yield # receive value
print(f"Echo: {value}")
# Create and prime generator
gen = echo()
next(gen) # prime it (run until first yield)
# Send values
message =
gen.send(message)
gen.send(42)
gen.send([1, 2, 3])
# Basic send()
def echo():
"""Generator that echoes received values"""
while True:
value = yield # receive value
print(f"Echo: {value}")
# Create and prime generator
gen = echo()
next(gen) # prime it (run until first yield)
# Send values
message =
gen.send(message)
gen.send(42)
gen.send([1, 2, 3])
priming.py
# Priming a generator
def receiver():
print("Generator started")
while True:
value = yield
print(f"Received: {value}")
# Without priming (error)
gen = receiver()
try:
gen.send(10) # ERROR: can't send to a just-started generator
except TypeError as e:
print(f"Error: {e}")
# With priming
gen = receiver()
next(gen) # prime it - runs until first yield
gen.send(10)
gen.send(20)
bidirectional.py
# Send and receive
def running_average():
"""Calculate running average of sent values"""
total = 0
count = 0
while True:
value = yield total / count if count > 0 else 0
total += value
count += 1
# Use bidirectional generator
gen = running_average()
print("Initial:", next(gen)) # prime and get initial value
print("After 10:", gen.send(10))
print("After 20:", gen.send(20))
print("After 30:", gen.send(30))
state_machine.py
# Stateful coroutine
def traffic_light():
"""Traffic light state machine"""
state = "red"
while True:
command = yield state
if command == "next":
if state == "red":
state = "green"
elif state == "green":
state = "yellow"
elif state == "yellow":
state = "red"
elif command == "reset":
state = "red"
# Use state machine
light = traffic_light()
print("Initial:", next(light))
print("Next:", light.send("next"))
print("Next:", light.send("next"))
print("Next:", light.send("next"))
print("Reset:", light.send("reset"))
close_throw.py
# Closing and throwing
def logger():
"""Generator that logs messages"""
try:
while True:
msg = yield
print(f"[LOG] {msg}")
except GeneratorExit:
print("[LOG] Shutting down")
# Use close()
gen = logger()
next(gen)
gen.send("Starting")
gen.send("Processing")
gen.close() # triggers GeneratorExit
print("Generator closed")
# Use throw()
gen2 = logger()
next(gen2)
gen2.send("Message 1")
try:
gen2.throw(ValueError, "Simulated error")
except ValueError:
print("Caught exception from generator")
How It Works
- Call
next(gen)to start the generator (prime it) - Generator runs until
yield - Call
gen.send(value)to resume and pass a value yieldexpression evaluates to the sent value- Generator continues until next
yield
Use Cases
- Coroutines: functions that can pause and receive input
- Pipelines: processing data with feedback
- State machines: receive commands, send responses
send() - a method that resumes a generator and passes a value to the yield expression
priming - calling next() once to advance the generator to its first yield before using send()
yield expression - `value = yield result` both sends a result out and receives the next sent value
close() and throw() - methods to gracefully shut down a generator or inject exceptions
Exercise: practical.py
Build a running average calculator using generator send()