1.4.2 · D5Python & Scientific Computing

Question bank — Functions, classes, and modules

1,470 words7 min readBack to topic

Before we start, three plain-word anchors the questions lean on:


True or false — justify

A default parameter value like def f(x, cache=[]) is created fresh every time the function is called
False. The default is built ==once, when the def line runs==, and reused across calls — so a mutable default like [] accumulates data between calls. Use cache=None then if cache is None: cache = [] inside.
return self inside fit is required for the method to actually work
False. It works fine without it; return self only enables method chaining like model.fit(X, y).predict(X_test), a convenience scikit-learn adopts, not a correctness need.
Two DenseLayer(100, 50) objects share the same weights because they came from the same class
False. Each call to __init__ builds a separate set of instance attributes; the class is only the blueprint, and each object owns its own self.weights.
Passing a NumPy array into a function and modifying it inside changes the caller's array
True. Arrays are mutable and passed by reference, so in-place edits (X[X==0] = 1) are visible outside; slicing/reassigning a new array (X = X + 1) is not.
A method must always be called as obj.method(); you can never call it via the class name
False. DenseLayer.forward(layer1, x) is legal and does exactly the same thing — obj.method(x) is just sugar that fills self with obj.
Adding a docstring changes how the function runs
False. The docstring is metadata only; it runs nothing, but tools and help() read it, so it changes how humans and IDEs understand the function, not the computation.
Importing a module runs its top-level code every time you write import mymodule in different files
False. Python caches modules after the first import (in sys.modules); later imports reuse the cached, already-executed version.
def compute_mse(y_true, y_pred, squared=True) lets you call compute_mse(y_pred=p, y_true=t)
True. Keyword arguments can be passed in any order, because they are matched by name, not position — this is exactly why keyword args exist.
A method with a leading underscore like _euclidean_distance cannot be called from outside the class
False. Python has no true privacy; the underscore is a convention saying "internal, don't rely on this," but the method is still fully callable.

Spot the error

def add_item(item, bag=[]): bag.append(item); return bag — what breaks?
The shared mutable default: bag is the same list on every call, so items pile up across unrelated calls. Fix with bag=None and create a new list inside.
class Layer: def __init__(input_size): self.size = input_size — why does creating Layer(50) fail?
self is missing from the parameter list. Python passes the object as the first argument automatically, so input_size receives the object and 50 has nowhere to go (argument-count error).
def forward(self, x): return x @ weights + biases inside a class — why is weights undefined?
Instance data must be reached through self. Bare weights looks for a global or local variable, not the attribute, so it should be self.weights and self.biases.
X_test_norm, _, _ = normalize_features(X_test) in the parent's example — what silent bug does this cause?
It recomputes min/max from the test set instead of reusing training stats, which is data leakage; the correct call passes the saved mins, maxs.
def compute_mse(y_true=[], y_pred, squared=True) — why won't this def even parse?
A parameter with a default cannot come before one without a default. Non-default parameters must all appear first in the signature.
range_vals = feature_max - feature_min then dividing without the range_vals[range_vals==0]=1 line — what fails?
A constant feature gives range 0, so (X - min) / 0 produces inf/nan. The guard replaces zero ranges with 1 so those columns normalize to 0 safely.
k = 3 written directly in the class body (not in __init__) — how does this differ from self.k = 3?
It becomes a class attribute shared by all instances instead of a per-object value; changing it via the class affects every object that hasn't overridden it.

Why questions

Why parameterize a function instead of reading global variables like the naive MSE version?
So the function works on any dataset passed in, is testable in isolation, and has no hidden dependency on outside state — the whole point of reuse.
Why does a class beat a dictionary for a neural network layer?
A class guarantees the expected attributes exist (via __init__) and bundles behavior with data, while a dict has no key guarantees and needs behavior scattered in loose functions.
Why does layer1.forward(x) "know" to use layer1's weights and not layer2's?
Because layer1 is passed in as ==self==, so self.weights resolves to that specific object's weights — method binding carries the object automatically.
Why put initialization logic in __init__ rather than expecting the user to set attributes by hand?
__init__ runs guaranteed at creation time, so every object is born fully valid and consistent, instead of relying on the caller to remember every field.
Why keep normalization as a function that returns the min/max rather than storing them globally?
So the exact training statistics can be reused on the test set explicitly and traceably, avoiding leakage and making the data flow visible.
Why split code into modules (files) at all if functions and classes already organize logic?
Modules group related functions/classes by concern (preprocessing.py, metrics.py), controlling namespaces and imports so large projects stay navigable — see Software engineering for ML.
Why prefer keyword defaults (like squared=True) over separate functions mse() and rmse()?
They share one tested code path with a small variation, so a bug in the shared math is fixed once, not in two divergent copies.

Edge cases

What happens if you call normalize_features(X) where every column is constant?
Every range is 0, replaced by 1 by the guard, so X_norm becomes all zeros — no crash, and a sensible "no variation" result.
What does KNNClassifier(k=len(X_train)) predict for every point?
With k equal to the whole training set, every prediction becomes the global majority class, since all neighbors are always included — the model degenerates to "predict the most common label."
What if fit is never called before predict?
self.X_train is still None (its __init__ value), so distance computation ==fails on None==; this is why lazy learners must be fitted before use.
A function ends without any return statement — what does calling it give you?
It returns ==None== implicitly. Forgetting return in a metric function is a classic trap: downstream math on None then errors.
You define two methods with the same name in one class — which one wins?
The second definition silently replaces the first; Python keeps only the last, with no warning, so accidental duplicate method names are easy to miss.
Calling compute_mse([], []) on empty inputs — what is the failure mode?
Dividing by len(y_true) which is 0 causes a ZeroDivisionError; robust functions should guard against empty inputs or document the precondition.
Recall Quick self-test

Mutable default trap — what causes it? ::: The default value is created once at definition time and shared across all calls. What secretly fills self? ::: The object on the left of the dot in obj.method(). Why return the training min/max from a normalizer? ::: To reuse identical stats on the test set and prevent data leakage.