Python Generators Explained for SimPy Users
SimPy is built on Python generators. If generators feel mysterious, SimPy will too. Let's fix that.
What Is a Generator?
A generator is a function that can pause and resume. Instead of return, it uses yield.
def simple_generator():
yield 1
yield 2
yield 3
Calling it doesn't run it—it creates a generator object:
gen = simple_generator()
print(type(gen)) # <class 'generator'>
Running a Generator
Use next() to advance to the next yield:
gen = simple_generator()
print(next(gen)) # 1
print(next(gen)) # 2
print(next(gen)) # 3
print(next(gen)) # StopIteration error
Each next():
1. Runs until yield
2. Returns the yielded value
3. Pauses
Generators Remember State
This is the magic. Local variables persist:
def counter():
count = 0
while True:
count += 1
yield count
gen = counter()
print(next(gen)) # 1
print(next(gen)) # 2
print(next(gen)) # 3
count isn't reset between calls. The generator remembers where it was.
How SimPy Uses This
SimPy processes are generators that yield events:
def process(env):
print("Start")
yield env.timeout(5) # Pause here
print("Middle")
yield env.timeout(5) # Pause here
print("End")
SimPy:
1. Calls next() to start—prints "Start", gets timeout, pauses
2. Waits until time 5
3. Calls next() again—prints "Middle", gets timeout, pauses
4. Waits until time 10
5. Calls next() again—prints "End", generator completes
The process maintains its state (local variables, position) between yields.
Yield Can Receive Values
Generators can receive values when resumed:
def echo():
while True:
received = yield
print(f"Received: {received}")
gen = echo()
next(gen) # Start the generator (runs to first yield)
gen.send("Hello") # Received: Hello
gen.send("World") # Received: World
SimPy uses this to pass event results back:
def process(env):
# yield returns the event value when resumed
result = yield some_process
print(f"Process returned: {result}")
Generator Functions vs Regular Functions
| Regular Function | Generator Function |
|---|---|
| Runs to completion | Can pause and resume |
Uses return |
Uses yield |
| Returns a value | Returns a generator object |
| Memory freed after return | State preserved between yields |
Creating Multiple Generators
Each call creates an independent generator:
def counter():
count = 0
while True:
count += 1
yield count
gen1 = counter()
gen2 = counter()
print(next(gen1)) # 1
print(next(gen1)) # 2
print(next(gen2)) # 1 (independent!)
print(next(gen1)) # 3
In SimPy, each process is an independent generator:
env.process(customer(env, "Alice")) # One generator
env.process(customer(env, "Bob")) # Different generator
Yield From
yield from delegates to another generator:
def inner():
yield 1
yield 2
def outer():
yield from inner() # Runs inner completely
yield 3
gen = outer()
print(list(gen)) # [1, 2, 3]
In SimPy, use for sub-processes:
def prepare(env):
yield env.timeout(5)
def execute(env):
yield env.timeout(10)
def main(env):
yield from prepare(env)
yield from execute(env)
Generators Are Lazy
Generators compute values on demand:
def big_range():
i = 0
while True:
yield i
i += 1
gen = big_range()
# No memory used yet!
print(next(gen)) # 0 - computed now
print(next(gen)) # 1 - computed now
SimPy benefits from this—thousands of processes don't consume excessive memory.
Generator Methods
Generators have special methods:
gen = my_generator()
next(gen) # Advance to next yield
gen.send(value) # Resume with a value
gen.throw(Error) # Throw exception at current yield
gen.close() # Stop the generator
SimPy uses these internally to control processes.
Exception Handling in Generators
Generators handle exceptions normally:
def process():
try:
yield 1
yield 2
except ValueError:
yield "Caught!"
gen = process()
print(next(gen)) # 1
print(gen.throw(ValueError)) # Caught!
SimPy uses this for interrupts:
def interruptible(env):
try:
yield env.timeout(100)
except simpy.Interrupt:
print("Interrupted!")
Practical Example: SimPy-Style Without SimPy
Here's a simplified simulation using pure generators:
import heapq
def process1():
print("P1: Start at 0")
yield 5
print("P1: Resume at 5")
yield 3
print("P1: Resume at 8")
def process2():
print("P2: Start at 0")
yield 2
print("P2: Resume at 2")
yield 10
print("P2: Resume at 12")
# Simple event loop
events = [] # (time, process)
now = 0
for p in [process1(), process2()]:
delay = next(p)
heapq.heappush(events, (now + delay, p))
while events:
now, proc = heapq.heappop(events)
try:
delay = proc.send(None)
heapq.heappush(events, (now + delay, proc))
except StopIteration:
pass
This is essentially what SimPy does, with more features.
Summary
Generators:
- Are functions that pause (yield) and resume
- Maintain state between pauses
- Create independent instances on each call
- Can receive values via send()
- Are the foundation of SimPy processes
Understand generators. SimPy follows naturally.
Next Steps
Discover the Power of Simulation
Want to become a go-to expert in simulation with Python? The Complete Simulation Bootcamp will show you how simulation can transform your career and your projects.
Explore the Bootcamp