Day 18 – Context Managers

 

📦 Day 18 – Context Managers

1. Learning Objectives

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

  • Understand what context managers are and why they simplify resource management in AEC scripts.
  • Use the with statement for files, database connections, and temporary settings.
  • Create your own context managers using __enter__ and __exit__ methods.
  • Use contextlib utilities (@contextmanager decorator) for simpler creation.
  • Apply context managers to practical AEC tasks: safely opening model files, temporary unit conversion context, automatic timing of analysis runs, and saving/restoring plot settings.

2. Concept Explanation

2.1 Why Context Managers in AEC?

In AEC programming, you often work with resources that need careful setup and teardown:

  • Files: Open a CSV → read data → close the file. If something crashes mid‑read, the file must still close.
  • Plotting: Set matplotlib style → draw → restore original style.
  • Temporary settings: Switch to metric units → perform calculations → switch back.
  • Database connections: Connect to a material database → query → disconnect.

Context managers automate the "setup → teardown" pattern, ensuring cleanup even if an error occurs.

2.2 The with Statement and Built‑in Context Managers

The most familiar context manager is open():

# Without context manager (error‑prone)
f = open("data.csv", "r")
content = f.read()
f.close()   # What if an exception happens before this line?

# With context manager (safe and clean)
with open("data.csv", "r") as f:
    content = f.read()
# File is automatically closed, even if an exception occurs

2.3 Creating a Context Manager with a Class

Define a class with __enter__ and __exit__ methods.

class UnitConverter:
    """Context manager that temporarily switches units."""
    def __enter__(self):
        print("Switching to metric units...")
        # Could modify global settings here
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        print("Restoring original units...")
        # Clean up even if an error occurred
        # Return False to propagate any exception (default is False)
        return False

with UnitConverter():
    print("  Performing calculations in metric...")
print("Back to project units.")

2.4 Using @contextmanager from contextlib

For simple cases, you can use a generator function with the @contextmanager decorator:

from contextlib import contextmanager

@contextmanager
def temporary_setting(name, value):
    """Temporarily change a setting and restore it afterwards."""
    original = getattr(some_module, name)   # save original
    setattr(some_module, name, value)       # apply new value
    try:
        yield   # the code inside `with` block runs here
    finally:
        setattr(some_module, name, original)  # restore original

2.5 Multiple Context Managers

You can nest or combine them in one with:

with open("input.csv", "r") as infile, open("output.csv", "w") as outfile:
    data = infile.read()
    outfile.write(data.upper())

3. Code Examples

Example 1: Timing context manager for benchmarking

import time

class Timer:
    """Context manager that times the execution of a block."""
    def __enter__(self):
        self.start = time.time()
        return self   # so we can access .elapsed after
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        self.end = time.time()
        self.elapsed = self.end - self.start
        print(f"Block took {self.elapsed:.4f} seconds")
        return False  # don't suppress exceptions

# Usage
with Timer() as t:
    total = sum(i**2 for i in range(10_000_000))
print(f"Sum = {total}, Elapsed = {t.elapsed:.4f}s")

Example 2: Safe CSV reading with error logging context manager

import csv
import sys

class CSVReader:
    """Context manager for robust CSV reading with error logging."""
    def __init__(self, filename):
        self.filename = filename
        self.file = None
    
    def __enter__(self):
        try:
            self.file = open(self.filename, "r", newline="")
            self.reader = csv.DictReader(self.file)
            print(f"Opened {self.filename} successfully.")
            return self.reader
        except FileNotFoundError:
            print(f"ERROR: File {self.filename} not found.")
            sys.exit(1)   # or raise
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        if self.file:
            self.file.close()
            print(f"Closed {self.filename}.")
        if exc_type is not None:
            print(f"An error occurred: {exc_val}")
        return False  # re‑raise any exception

# Usage
with CSVReader("beams.csv") as reader:
    for row in reader:
        span = float(row["Span_m"])
        if span > 8.0:
            print(f"Long span: {row['Mark']} = {span}m")

Example 3: Temporary matplotlib style context manager

import matplotlib.pyplot as plt
from contextlib import contextmanager

@contextmanager
def plot_style(style="seaborn-v0_8-whitegrid"):
    """Temporarily change matplotlib style."""
    original = plt.rcParams.copy()
    plt.style.use(style)
    try:
        yield
    finally:
        plt.rcParams.update(original)

# Usage
with plot_style("ggplot"):
    fig, ax = plt.subplots()
    ax.plot([0, 1, 2], [0, 1, 4])
    ax.set_title("ggplot style")
    plt.show()
# Style is restored automatically

Example 4: Temporary working directory context manager

import os
from contextlib import contextmanager

@contextmanager
def working_directory(path):
    """Temporarily change the current working directory."""
    original = os.getcwd()
    os.chdir(path)
    try:
        yield
    finally:
        os.chdir(original)

# Usage
with working_directory("/tmp/project_data"):
    # All file operations happen in /tmp/project_data
    with open("temp_output.txt", "w") as f:
        f.write("Hello from temp directory")
# Now we're back in original directory

Example 5: Context manager for database connection (simulated)

class MaterialDatabase:
    """Simulated connection to a material properties database."""
    def __init__(self, db_name):
        self.db_name = db_name
        self.connected = False
    
    def __enter__(self):
        print(f"Connecting to {self.db_name}...")
        self.connected = True
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        print(f"Disconnecting from {self.db_name}...")
        self.connected = False
        if exc_type:
            print(f"DB error occurred: {exc_val}")
        return False
    
    def query(self, material):
        if not self.connected:
            raise RuntimeError("Not connected to database")
        # Simulated lookup
        database = {
            "Steel": {"density": 7850, "E": 200000},
            "Concrete": {"density": 2400, "E": 30000},
        }
        return database.get(material, {"density": 0, "E": 0})

# Usage
with MaterialDatabase("AISC_Steel_DB") as db:
    steel_props = db.query("Steel")
    print(f"Steel density: {steel_props['density']} kg/m³")
# Connection automatically closed

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

Problem 1 – File write with automatic closure
Write a simple script that uses with open(...) to write three lines of text to "report.txt". Then read the file back (also with with) and print its contents.

Problem 2 – Timer context manager
Create a context manager Timer (using a class or @contextmanager) that measures the time of the enclosed block. Use it to time a loop that computes the sum of squares from 1 to 1,000,000.

Problem 3 – Temporary precision change
Imagine a global variable PRECISION = 2. Write a context manager temporary_precision(n) that temporarily changes PRECISION to n inside the with block, then restores it. Demonstrate by printing a formatted number inside and outside the block.

Problem 4 – Safe plot export
Write a context manager save_plot(filename) that:

  • On entry: creates a new matplotlib figure.
  • On exit: saves the figure to filename and closes it (using plt.close()).
  • Handles errors during save gracefully.

Test by plotting a simple line and saving as "test_plot.png".

Solutions (attempt first):

# P1
with open("report.txt", "w") as f:
    f.write("Beam analysis report\n")
    f.write("Span: 6.0 m\n")
    f.write("Load: 25 kN/m\n")
with open("report.txt", "r") as f:
    print(f.read())

# P2
import time
class Timer:
    def __enter__(self):
        self.start = time.time()
        return self
    def __exit__(self, *args):
        self.end = time.time()
        self.elapsed = self.end - self.start
        print(f"Elapsed: {self.elapsed:.4f}s")
        return False

with Timer():
    total = sum(x**2 for x in range(1_000_001))
print(f"Sum of squares: {total}")

# P3
PRECISION = 2
from contextlib import contextmanager
@contextmanager
def temporary_precision(n):
    global PRECISION
    original = PRECISION
    PRECISION = n
    try:
        yield
    finally:
        PRECISION = original

value = 3.14159
print(f"Outside: {value:.{PRECISION}f}")  # 3.14
with temporary_precision(4):
    print(f"Inside: {value:.{PRECISION}f}")  # 3.1416
print(f"Outside again: {value:.{PRECISION}f}")  # 3.14

# P4
import matplotlib.pyplot as plt
class save_plot:
    def __init__(self, filename):
        self.filename = filename
    def __enter__(self):
        self.fig, self.ax = plt.subplots()
        return self.ax
    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type is None:
            self.fig.savefig(self.filename, dpi=150)
            print(f"Saved plot to {self.filename}")
        else:
            print(f"Error during plotting: {exc_val}")
        plt.close(self.fig)
        return False

with save_plot("test_plot.png") as ax:
    ax.plot([0,1,2], [0,1,4], 'b-o')
    ax.set_title("Test")

5. Applied Challenge Task

Task: Robust Model Export Context Manager

Design a context manager model_export that manages the entire workflow of exporting a structural model to a CSV file for CAD import.

The context manager should:

  1. On entry (__enter__):

    • Ask the user for an export filename (or accept as argument).
    • Open the file for writing.
    • Write a header row: "ELEMENT_TYPE,ID,LEVEL,X_START,Y_START,X_END,Y_END,MATERIAL"
    • Print a message indicating export started.
    • Return a writer object (or a custom handler).
  2. Inside the with block, the user can call a method write_element(etype, eid, level, x1, y1, x2, y2, material) that writes one row.

  3. On exit (__exit__):

    • Ensure the file is properly closed.
    • Print a summary of how many elements were exported.
    • If an exception occurred during export (e.g., bad data), catch it, log the error, and optionally save what was written so far (do not delete the file).

Bonus:
Add a nested context manager for temporary unit conversion: inside the with model_export(...) block, use another context manager metric_units that temporarily sets a global UNITS = "metric" flag. The write_element method should check this flag to decide if coordinates need conversion.

Why this matters:
Context managers are ideal for workflows with a clear setup/teardown pattern: opening databases, exporting reports, batch processing files. This task models a real BIM‑CAD export pipeline.


6. Brief Review Summary

  • Context managers automate resource setup and teardown using with blocks.
  • Built‑in example: with open(...) as f: – file auto‑closes.
  • Create custom managers: class with __enter__ and __exit__, or @contextmanager decorator on a generator.
  • __exit__ receives exception info – you can handle, log, or suppress errors.
  • Multiple context managers can be combined in one with statement.
  • Use cases in AEC: file I/O, database connections, temporary settings, timing, plotting.

Key takeaway:
Context managers make your code safer and more readable by ensuring cleanup always happens – critical when dealing with files, databases, or graphics in engineering automation.


7. Preview of Next Topic (Day 19)

Tomorrow we dive into BIM Data with IfcOpenShell.
You’ll learn:

  • What IFC (Industry Foundation Classes) is and why it matters for interoperability.
  • How to install and use IfcOpenShell to read .ifc files.
  • Extracting building elements (walls, slabs, columns) and their properties.
  • Querying spatial structure (site, building, storey).
  • A practical exercise: read an IFC file, list all walls on a given floor, and compute total wall volume.

This is your first step into real BIM data – a critical skill for modern AEC practice.

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