Day 16 – Iterators and Generators


♾️ Day 16 – Iterators and Generators

1. Learning Objectives

By the end of Day 16, you will be able to:

  • Understand the difference between iterable, iterator, and generator.
  • Create generator functions using yield to lazily produce sequences.
  • Use generators to efficiently traverse large datasets (e.g., IFC models, point clouds).
  • Build infinite sequences and pipeline data processing.
  • Apply generators to AEC tasks: lazy reading of building elements, generating column grids, streaming coordinates.

2. Concept Explanation

2.1 Why Generators Matter in AEC

Large building models can have tens of thousands of elements – walls, beams, columns, rooms. Loading everything into memory at once is inefficient. Generators let you:

  • Process one element at a time – memory efficient.
  • Chain processing steps – filter, transform, analyse in a pipeline.
  • Generate infinite sequences – e.g., parametric numbering.
  • Lazy evaluation – compute values only when needed.

2.2 Iterables vs Iterators vs Generators

ConceptDescriptionExample
IterableAn object that can be looped over (__iter__)list, tuple, str, range
IteratorAn object that produces values one at a time (__next__)iter([1,2,3])
GeneratorA special iterator created by a function with yielddef gen(): yield 1

2.3 Generator Functions with yield

A generator function uses yield instead of return. It pauses execution, yields a value, and resumes when the next value is requested.

def floor_levels(start, count, step):
    """Generate floor elevations lazily."""
    for i in range(count):
        yield start + i * step

# Usage
for level in floor_levels(0.0, 5, 3.5):
    print(f"Floor at {level:.1f} m")

Each call to next() resumes the function until the next yield.

2.4 Generator Expressions

Like list comprehensions but with parentheses – memory efficient.

# List comprehension – builds entire list in memory
squares_list = [x**2 for x in range(10_000)]

# Generator expression – yields values one at a time
squares_gen = (x**2 for x in range(10_000))

# Use in a loop
for sq in squares_gen:
    if sq > 100:
        break
    print(sq)

2.5 Chaining Generators (Pipelines)

You can chain generators to build data processing pipelines:

def read_coordinates(file):
    for line in open(file):
        if line.strip():
            x, y = map(float, line.split(","))
            yield x, y

def filter_within_bbox(points, xmin, ymin, xmax, ymax):
    for x, y in points:
        if xmin <= x <= xmax and ymin <= y <= ymax:
            yield x, y

def convert_to_mm(points):
    for x, y in points:
        yield x * 1000, y * 1000

# Pipeline: read → filter → convert
points = read_coordinates("columns.csv")
filtered = filter_within_bbox(points, 0, 0, 50, 50)
mm_points = convert_to_mm(filtered)
for x_mm, y_mm in mm_points:
    print(f"({x_mm:.0f}, {y_mm:.0f})")

3. Code Examples

Example 1: Lazy column grid generator

def column_grid(x_count, y_count, x_spacing, y_spacing):
    """Generate (x, y) coordinates for a column grid, one at a time."""
    for row in range(y_count):
        for col in range(x_count):
            yield (col * x_spacing, row * y_spacing)

# Use – no large list created
for i, (x, y) in enumerate(column_grid(4, 3, 6.0, 8.0)):
    print(f"Column {i+1}: ({x:.1f}, {y:.1f})")
    if i >= 5:  # only need first 6
        break

Example 2: Lazy reading of building elements (simulating IFC traversal)

def read_elements(csv_file):
    """Simulate lazy reading of building elements from a CSV."""
    import csv
    with open(csv_file, "r") as f:
        reader = csv.DictReader(f)
        for row in reader:
            # Simulate processing delay
            yield {
                "id": row["ID"],
                "type": row["Type"],
                "level": int(row["Level"]),
                "volume": float(row["Volume_m3"])
            }

# Usage – process one element at a time
total_volume = 0.0
wall_count = 0
for elem in read_elements("building_elements.csv"):
    if elem["type"] == "Wall":
        total_volume += elem["volume"]
        wall_count += 1
        print(f"Processing wall {elem['id']}...")

print(f"\nTotal wall volume: {total_volume:.2f} m³ ({wall_count} walls)")

Example 3: Infinite generator for beam numbering

def beam_number_generator(prefix="B"):
    """Generate sequential beam marks: B1, B2, B3, ... infinitely."""
    n = 1
    while True:
        yield f"{prefix}{n}"
        n += 1

# Use with a finite loop
beams = beam_number_generator()
for i in range(5):
    print(next(beams))

# Output: B1, B2, B3, B4, B5

Example 4: Pipeline for coordinate transformation

def read_coordinates(filename):
    """Read (x, y) pairs from a CSV file, one at a time."""
    with open(filename, "r") as f:
        for line in f:
            if line.strip() and not line.startswith("x"):
                parts = line.split(",")
                yield float(parts[0]), float(parts[1])

def rotate_points(points, angle_deg):
    """Rotate points by given angle (degrees)."""
    import math
    rad = math.radians(angle_deg)
    cos_a, sin_a = math.cos(rad), math.sin(rad)
    for x, y in points:
        yield (x * cos_a - y * sin_a, x * sin_a + y * cos_a)

def shift_points(points, dx, dy):
    """Shift points by (dx, dy)."""
    for x, y in points:
        yield (x + dx, y + dy)

# Build pipeline
points = read_coordinates("columns.csv")
rotated = rotate_points(points, 45.0)
shifted = shift_points(rotated, 10.0, 5.0)

for x, y in shifted:
    print(f"Transformed: ({x:.2f}, {y:.2f})")

4. Hands‑on Exercises (3–5 Problems)

Problem 1 – Floor level generator
Write a generator floor_levels(base, height, num_floors) that yields the elevation of each floor. Test with base=0, height=3.5, num_floors=10.

Problem 2 – Filter long spans with a generator
Given a list spans = [6.0, 4.5, 7.2, 3.0, 8.1, 5.5], write a generator long_spans(spans, min_span=6.0) that yields only spans >= min_span. Use it in a loop.

Problem 3 – Fibonacci for structural load distribution (bonus concept)
Write a generator that yields Fibonacci numbers indefinitely. Use it to generate the first 15 Fibonacci numbers and print them.

Problem 4 – Generator expression for square metres
Given a list of room dimensions as tuples (length, width):
rooms = [(8,5), (6,4), (10,6), (4,3)]
Use a generator expression to compute areas, then print only areas > 30 m².

Solutions (attempt first):

# P1
def floor_levels(base, height, num_floors):
    for i in range(num_floors):
        yield base + i * height

for level in floor_levels(0, 3.5, 10):
    print(f"Level at {level:.1f} m")

# P2
def long_spans(spans, min_span=6.0):
    for s in spans:
        if s >= min_span:
            yield s

spans = [6.0, 4.5, 7.2, 3.0, 8.1, 5.5]
for s in long_spans(spans):
    print(f"Long span: {s} m")

# P3
def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

fib = fibonacci()
for _ in range(15):
    print(next(fib), end=" ")
print()

# P4
rooms = [(8,5), (6,4), (10,6), (4,3)]
areas = (l * w for l, w in rooms)
large_areas = (a for a in areas if a > 30)
for a in large_areas:
    print(f"Area: {a} m²")

5. Applied Challenge Task

Task: Lazy IFC‑like Element Processor

You are given a CSV file building_elements.csv with columns:

ID,Type,Level,Length_m,Width_m,Height_m,Material
W1,Wall,0,6.0,0.2,3.0,Concrete
W2,Wall,0,4.5,0.2,3.0,Concrete
C1,Column,0,0.4,0.4,3.5,Steel
S1,Slab,0,12.0,8.0,0.2,Concrete
W3,Wall,1,8.0,0.2,2.8,Brick
W4,Wall,1,5.0,0.2,2.8,Brick
C2,Column,1,0.5,0.5,2.8,Steel
S2,Slab,1,12.0,8.0,0.2,Concrete

Write a script that:

  1. Defines a generator read_elements(filename) that yields each row as a dictionary lazily.

  2. Defines a generator filter_by_type(elements, element_type) that yields only elements of a given type.

  3. Defines a generator filter_by_level(elements, level) that yields only elements on a given level.

  4. Defines a generator compute_volume(elements) that adds a volume key (for walls: L×W×H; columns: L×W×H; slabs: L×W×H) and yields the enriched dictionary.

  5. Uses this pipeline to:

    • Find total volume of all concrete elements on level 0.
    • Find total volume of all walls on level 1.
    • Count the number of steel columns overall.

Bonus:
Wrap the pipeline in a with statement (using context manager from Day 18 concept) or at minimum add proper error handling for missing files.

Why this matters:
Real IFC models can have hundreds of thousands of elements. Using generators, you can process them without loading everything into memory – a critical skill for BIM data analysis.


6. Brief Review Summary

  • Generators (yield) produce values lazily – memory efficient for large datasets.
  • Generator expressions (x for x in iterable) are like lazy list comprehensions.
  • Chaining generators creates data processing pipelines.
  • Infinite generators produce sequences on demand (e.g., beam numbering).
  • Iterators and generators are fundamental for traversing large AEC models.

Key takeaway:
Generators allow you to process building models of any size without memory issues. Combined with pipeline chaining, they are a powerful tool for efficient AEC data processing.


7. Preview of Next Topic (Day 17)

Tomorrow we’ll cover Decorators.
You’ll learn:

  • How to wrap functions to add behaviour (timing, logging, caching).
  • Using @decorator syntax for clean code.
  • Practical AEC examples: timing analysis runs, caching heavy structural calculations, validating inputs.
  • Building a decorator that logs all function calls for audit trails.

Decorators are a key Python feature for writing clean, reusable cross‑cutting logic.

Comments

EARN AT THE COMFORT OF YOUR HOME

Sponsored content

Popular posts from this blog

Day 5 – Functions, Scope, and Docstrings

Day 10 – Exception Handling and Debugging

Day 1 – Python Foundations for AEC Professionals