Exercises

ex-sp-ch02-01

Easy

Write a pure function standardize(x: np.ndarray) -> np.ndarray that returns an array with zero mean and unit variance. Verify that it does not modify the input array. Add a complete NumPy-style docstring.

ex-sp-ch02-02

Easy

Write a function apply_transforms(data, *transforms) that takes a NumPy array and applies a series of transformation functions in order. Use *args to accept any number of transforms.

ex-sp-ch02-03

Easy

Fix the following function that has the mutable default argument bug:

def log_experiment(result, history=[]):
    history.append(result)
    return history

Demonstrate the bug and write the corrected version.

ex-sp-ch02-04

Easy

Write a simple @timer decorator that prints the execution time of any function. Use functools.wraps to preserve metadata. Test it on a function that computes the eigenvalues of a random matrix.

ex-sp-ch02-05

Easy

Write a context manager using contextlib.contextmanager that prints "Starting..." when entering and "Done in X.XXXXs" when exiting. Test it on a NumPy matrix multiplication.

ex-sp-ch02-06

Medium

Create a function factory make_activation(name: str) that returns activation functions commonly used in neural networks. Support "relu", "sigmoid", "tanh", and "leaky_relu" (with a configurable alpha parameter for leaky ReLU). Each returned function should operate on NumPy arrays.

ex-sp-ch02-07

Medium

Write a @count_calls decorator that tracks how many times a function has been called. The count should be accessible via func.call_count and resettable via func.reset_count().

ex-sp-ch02-08

Medium

Write a decorator factory @retry(max_attempts=3, delay=1.0) that retries a function if it raises an exception, with exponential backoff. This is useful for network calls or flaky I/O operations in scientific pipelines.

ex-sp-ch02-09

Medium

Create a class-based context manager WorkingDirectory that temporarily changes the working directory and restores it on exit:

with WorkingDirectory("/tmp/experiments"):
    # cwd is now /tmp/experiments
    save_results(data)
# cwd is restored to original

Then rewrite it using @contextmanager.

ex-sp-ch02-10

Medium

Rewrite the following loop-variable closure bug so that each callback correctly captures its own value. Provide three different solutions: default argument, functools.partial, and a factory function.

# Bug:
callbacks = []
for freq in [100, 200, 400, 800]:
    callbacks.append(lambda t: np.sin(2 * np.pi * freq * t))
# All callbacks use freq=800

ex-sp-ch02-11

Medium

Write a @validate_shapes decorator factory that checks NumPy array argument shapes before calling the function. Usage:

@validate_shapes({"X": (None, None), "y": (None,)})
def fit(X: np.ndarray, y: np.ndarray):
    ...

None means "any size in this dimension". The decorator should raise ValueError with a clear message if shapes do not match.

ex-sp-ch02-12

Hard

Implement a @memoize_disk decorator that caches function results to disk using pickle. The cache key should be based on the function name and arguments (handle NumPy arrays by hashing their contents). Include a cache_dir parameter and a way to invalidate the cache.

ex-sp-ch02-13

Hard

Write a Pipeline class that uses closures and the >> operator (via __rshift__) to compose data transformation functions:

pipe = Pipeline(remove_nans) >> log_transform >> normalize
result = pipe(raw_data)

The pipeline should support len() (number of steps), iteration over steps, and a describe() method that lists each function.

ex-sp-ch02-14

Hard

Implement a @deprecated(message, version) decorator factory that:

  1. Issues a DeprecationWarning on first call
  2. Includes the replacement function name in the message
  3. Only warns once per function (not on every call)
  4. Preserves the original function's metadata
@deprecated("Use compute_ber_v2 instead", version="3.0")
def compute_ber(tx, rx):
    ...

ex-sp-ch02-15

Hard

Build an ExitStack-based context manager that manages multiple resources dynamically. Write a function that opens N HDF5-like files (simulate with regular files), processes them, and ensures all are closed even if an error occurs partway through.

Use contextlib.ExitStack and demonstrate it with at least 5 files.

ex-sp-ch02-16

Hard

Write a @register decorator that registers functions in a dispatch table (dictionary). Then use it to build a simple plugin system for different optimization algorithms:

@register("sgd")
def sgd_optimizer(params, lr=0.01): ...

@register("adam")
def adam_optimizer(params, lr=0.001, beta1=0.9): ...

# Dispatch by name
optimizer = get_optimizer("adam")

ex-sp-ch02-17

Challenge

Build a complete @experiment_tracker decorator that:

  1. Logs function name, arguments, start time, end time, and duration
  2. Captures the return value and any exceptions
  3. Saves all runs to a JSON file with unique run IDs (UUID)
  4. Supports nested tracked functions (tracks the call tree)
  5. Provides a report() class method to summarize all runs

This simulates a lightweight version of MLflow's tracking functionality.

ex-sp-ch02-18

Challenge

Implement a @jit_fallback decorator that tries to JIT-compile a function with Numba, but gracefully falls back to the pure Python version if Numba is not installed or compilation fails. It should:

  1. Detect whether Numba is available at import time
  2. If available, apply @numba.jit(nopython=True) with error handling
  3. If compilation fails, warn and use the original function
  4. Provide a .is_jitted attribute to check which version is active
  5. Include a benchmark method that compares JIT vs non-JIT performance

Test it on a Monte Carlo simulation function.