🎀 Day 17 – Decorators
1. Learning Objectives
By the end of Day 17, you will be able to:
- Understand what decorators are and why they are useful in AEC programming.
- Write simple decorators to add behaviour (timing, logging, validation) to existing functions.
- Use the
@decoratorsyntax for clean, readable code. - Apply decorators to practical AEC tasks: timing analysis runs, caching heavy structural calculations, validating inputs, and logging audit trails.
- Create decorators with arguments for flexible reuse.
2. Concept Explanation
2.1 What is a Decorator?
A decorator is a function that takes another function as input, wraps it with additional behaviour, and returns the wrapped function. In Python, you apply a decorator with the @ symbol above a function definition.
Think of it like this:
In an AEC office, you might have a standard "stamp" that you apply to all calculations – a note saying "Checked by Engineer" and the date. The calculation itself doesn't change, but it gains that extra annotation. A decorator does exactly that: it adds behaviour before, after, or around the original function.
2.2 The Basic Pattern
def my_decorator(func):
def wrapper(*args, **kwargs):
# Code to run BEFORE the original function
print(f"Calling function: {func.__name__}")
result = func(*args, **kwargs) # Call the original function
# Code to run AFTER the original function
print(f"Finished: {func.__name__}")
return result
return wrapper
@my_decorator
def say_hello(name):
print(f"Hello, {name}!")
say_hello("Engineer")
# Output:
# Calling function: say_hello
# Hello, Engineer!
# Finished: say_hello
2.3 When to Use Decorators in AEC
| Use Case | Description |
|---|---|
| Timing | Measure how long a heavy calculation takes |
| Logging | Log all function calls for audit trails |
| Validation | Check inputs are valid (e.g., span > 0) |
| Caching (memoization) | Store results of expensive computations |
| Retry logic | Retry a network call to a BIM server on failure |
| Authorization | Check user permissions before running a function |
2.4 Decorator with Arguments
Sometimes you need to pass arguments to the decorator itself (e.g., a log level, a threshold value).
def repeat(num_times):
"""Decorator that repeats a function call num_times times."""
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(num_times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(num_times=3)
def print_beam(beam_id):
print(f"Processing beam {beam_id}")
print_beam("B1")
# Output:
# Processing beam B1
# Processing beam B1
# Processing beam B1
3. Code Examples
Example 1: Timing decorator for structural analysis
import time
def timer(func):
"""Decorator that prints the execution time of a function."""
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
elapsed = end - start
print(f"[TIMER] {func.__name__} took {elapsed:.4f} seconds")
return result
return wrapper
@timer
def analyse_beam(span, load):
"""Simulate a heavy structural analysis calculation."""
# Simulate computation
total = 0
for i in range(1000000):
total += (load * span**2) / (8 * (i + 1))
return total
# Use it
result = analyse_beam(6.0, 25.0)
print(f"Result: {result:.2f}")
Example 2: Input validation decorator
def validate_positive(func):
"""Decorator that ensures all numeric arguments are positive."""
def wrapper(*args, **kwargs):
for arg in args:
if isinstance(arg, (int, float)) and arg <= 0:
raise ValueError(f"Argument {arg} must be positive")
for key, val in kwargs.items():
if isinstance(val, (int, float)) and val <= 0:
raise ValueError(f"Keyword argument {key}={val} must be positive")
return func(*args, **kwargs)
return wrapper
@validate_positive
def moment_udl(load, span):
"""Max bending moment for simply supported beam with UDL."""
return load * span**2 / 8
# Test
print(moment_udl(25, 6.0)) # Works
# print(moment_udl(-25, 6.0)) # Raises ValueError
# print(moment_udl(25, 0)) # Raises ValueError
Example 3: Logging decorator for audit trail
def log_call(func):
"""Decorator that logs each function call with timestamp."""
def wrapper(*args, **kwargs):
from datetime import datetime
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
arg_str = ", ".join([str(a) for a in args] + [f"{k}={v}" for k, v in kwargs.items()])
print(f"[LOG {timestamp}] {func.__name__}({arg_str})")
result = func(*args, **kwargs)
print(f"[LOG] → Result: {result:.4f}")
return result
return wrapper
@log_call
def deflection_check(load, span, E, I):
return 5 * load * 1000 * (span * 1000)**4 / (384 * E * I)
deflection_check(25.0, 6.0, 200000, 120e6)
# Output:
# [LOG 2026-05-06 13:20:00] deflection_check(25.0, 6.0, 200000, 120000000.0)
# [LOG] → Result: 7.0312
Example 4: Caching (memoization) decorator
def memoize(func):
"""Decorator that caches results of expensive function calls."""
cache = {}
def wrapper(*args):
if args in cache:
print(f"[CACHE] Returning cached result for {args}")
return cache[args]
result = func(*args)
cache[args] = result
print(f"[CACHE] Stored result for {args}")
return result
return wrapper
@memoize
def compute_section_modulus(b, h):
"""Expensive computation of section modulus (simulated delay)."""
import time
time.sleep(1) # Simulate heavy calculation
S = b * h**2 / 6
return S
# First call – computes and caches
S1 = compute_section_modulus(200, 400)
print(f"S1 = {S1:.0f} mm³")
# Second call with same args – uses cache (instant)
S2 = compute_section_modulus(200, 400)
print(f"S2 = {S2:.0f} mm³")
# Different args – computes again
S3 = compute_section_modulus(250, 500)
print(f"S3 = {S3:.0f} mm³")
Example 5: Decorator with arguments – repeat analysis
def repeat_analysis(num_runs):
"""Decorator that runs an analysis multiple times and averages."""
def decorator(func):
def wrapper(*args, **kwargs):
results = []
for i in range(num_runs):
result = func(*args, **kwargs)
results.append(result)
print(f" Run {i+1}: {result:.4f}")
avg = sum(results) / len(results)
print(f"Average over {num_runs} runs: {avg:.4f}")
return avg
return wrapper
return decorator
@repeat_analysis(num_runs=5)
def sample_deflection(load, span):
"""Simple deflection with slight random variation (simulating Monte Carlo)."""
import random
# Add ±5% random variation to simulate uncertainty
variation = 1 + random.uniform(-0.05, 0.05)
return (load * span**2 / 8) * variation
avg_M = sample_deflection(25.0, 6.0)
print(f"Average moment: {avg_M:.2f} kNm")
4. Hands‑on Exercises (3–5 Problems)
Problem 1 – Simple timing decorator
Write a decorator print_time that prints "Start" before a function runs and "End" after it finishes. Apply it to a function compute_area(length, width) that returns length * width. Test it.
Problem 2 – Validation decorator for beam depth
Write a decorator valid_depth that checks if the first argument (depth) is between 200 and 600 mm. If not, print a warning and return None instead of running the function. Apply it to a function classify_beam(depth) that returns a string classification.
Problem 3 – Retry decorator for network calls
Write a decorator retry(max_attempts=3) that retries a function if it raises an exception. Add a small delay between attempts. Apply it to a function fetch_material_data(material) that randomly fails (simulate with random.random()). Print how many attempts were needed.
Problem 4 – Logging all AEC calculations
Create a decorator log_to_file(filename) that appends a log entry to a text file each time the decorated function is called. The log entry should include: timestamp, function name, arguments, and result. Apply it to a moment_udl function and run it a few times. Check the file contents.
Solutions (attempt first):
# P1
def print_time(func):
def wrapper(*args, **kwargs):
print("Start")
result = func(*args, **kwargs)
print("End")
return result
return wrapper
@print_time
def compute_area(l, w):
return l * w
print(compute_area(8, 5))
# P2
def valid_depth(func):
def wrapper(depth, *args, **kwargs):
if depth < 200 or depth > 600:
print(f"WARNING: Depth {depth}mm out of range [200, 600]")
return None
return func(depth, *args, **kwargs)
return wrapper
@valid_depth
def classify_beam(depth):
if depth < 400:
return "Medium beam"
else:
return "Heavy beam"
print(classify_beam(350))
print(classify_beam(150))
# P3
import time, random
def retry(max_attempts=3):
def decorator(func):
def wrapper(*args, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
print(f"Attempt {attempt} failed: {e}")
if attempt < max_attempts:
time.sleep(0.5)
raise Exception(f"All {max_attempts} attempts failed")
return wrapper
return decorator
@retry(max_attempts=3)
def fetch_material_data(material):
if random.random() < 0.6: # 60% chance of failure
raise ConnectionError(f"Could not fetch {material}")
return f"{material} data loaded"
print(fetch_material_data("Steel"))
# P4
def log_to_file(filename="audit.log"):
def decorator(func):
def wrapper(*args, **kwargs):
from datetime import datetime
result = func(*args, **kwargs)
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
with open(filename, "a") as f:
f.write(f"[{timestamp}] {func.__name__}(args={args}, kwargs={kwargs}) → {result}\n")
return result
return wrapper
return decorator
@log_to_file("beam_calc_log.txt")
def moment_udl(load, span):
return load * span**2 / 8
moment_udl(25, 6.0)
moment_udl(18, 4.5)
# Check beam_calc_log.txt
5. Applied Challenge Task
Task: Engineering Calculation Manager with Decorators
Build a set of decorators and apply them to a small library of structural engineering functions.
Core functions to decorate:
def moment_udl(load, span):
return load * span**2 / 8
def shear_udl(load, span):
return load * span / 2
def deflection_udl(load, span, E, I):
return 5 * load * 1000 * (span * 1000)**4 / (384 * E * I)
def required_section_modulus(M, fy):
return M * 1e6 / (0.6 * fy)
Requirements:
- Timer decorator – Wrap all four functions with a timer that prints execution time to 4 decimal places.
- Input validation decorator – Ensure all numeric inputs are positive. If not, raise a
ValueErrorwith a clear message. - Logging decorator – Log each call to
"calc_log.csv"in CSV format:timestamp, function_name, arg1, arg2, ..., result. - Memoization decorator – Apply only to
deflection_udlsince it's the most expensive. Cache results keyed by(load, span, E, I). - Demonstrate all decorators working together – Call each function at least twice with the same and different arguments to show caching, timing, logging, and validation.
Bonus:
- Create a
@requires_units(units_dict)decorator that takes a dictionary specifying expected units for each parameter (e.g.,{"load": "kN/m", "span": "m"}) and prints a unit check message. - Use
functools.wrapsto preserve function metadata (name, docstring) in all decorators.
Why this matters:
In a real engineering office, calculation traceability, validation, and performance monitoring are essential. Decorators provide a clean, non‑invasive way to add these cross‑cutting concerns to existing code without modifying the core logic.
6. Brief Review Summary
- A decorator wraps a function to add behaviour before, after, or around it.
@decoratorsyntax is syntactic sugar forfunc = decorator(func).- Common AEC uses: timing, logging, validation, caching, retry.
- Decorators can accept arguments for flexibility.
- Use
functools.wrapsto preserve original function metadata. - Multiple decorators can stack:
@log @timer def calc(): ...
Key takeaway:
Decorators allow you to add professional‑grade features to your engineering functions – logging, validation, performance monitoring – without cluttering the core calculation logic. They keep your code clean and maintainable.
7. Preview of Next Topic (Day 18)
Tomorrow we’ll cover Context Managers.
You’ll learn:
- Using
withstatements for resource management (files, database connections, temporary settings). - Creating your own context managers with
__enter__and__exit__. - Using
contextlibfor simpler context manager creation. - Practical AEC examples: safely opening model files with automatic rollback, temporary unit conversion context, measuring execution time with a context manager.
Context managers make your code safer and more readable when dealing with resources.

Comments
Post a Comment