← Master Index
Vol. 03 Module 3.2 Lecture

Encapsulation

Object-Oriented Programming

How This Lesson Fits the Module

ML objects carry sensitive internal state: raw file paths, normalization statistics, cached tensors, random seeds, and half-initialized weights. If every caller can reach in and mutate those directly, reproducibility breaks and bugs hide.

Encapsulation bundles data with the methods that are allowed to change it—and hides the rest. You expose a clean get_batch() or predict() while keeping preprocessing, locking, and validation inside the object.

Learning Objectives

By the end of this lesson, students should be able to:

  • Explain encapsulation as hiding internal state and exposing a controlled public interface.
  • Use naming conventions (_single and __double leading underscores) to signal visibility in Python.
  • Implement @property getters and setters for validated attribute access.
  • Protect dataset invariants (label ranges, feature shapes) inside class methods.
  • Recognize encapsulation in framework design (e.g., nn.Module parameter registration).
  • Avoid leaking mutable internal references that let callers corrupt object state.

Hiding Implementation Details

Definition — Encapsulation

Encapsulation restricts direct access to an object’s internal representation. External code interacts through a public interface (methods and properties); internal attributes and helper logic stay private by convention or design.

Python does not enforce true privacy. Instead, conventions and properties provide discipline:

Convention Meaning ML Example
public_attr Part of the intended API dataset.transform
_protected Internal; subclasses may use self._cache of preloaded images
__private Name-mangled; harder to access accidentally self.__raw_paths not meant for callers

ML Example: Encapsulated Dataset

class NormalizedTabularDataset:
    def __init__(self, features, labels):
        self._validate(features, labels)
        self.__features = features          # internal storage
        self.__labels = labels
        self._mean, self._std = self._compute_stats(features)

    def _validate(self, features, labels):
        if len(features) != len(labels):
            raise ValueError("Length mismatch")
        if not features:
            raise ValueError("Empty dataset")

    def _compute_stats(self, features):
        import numpy as np
        arr = np.array(features, dtype=float)
        return arr.mean(axis=0), arr.std(axis=0) + 1e-8

    def __getitem__(self, index):
        import numpy as np
        x = np.array(self.__features[index], dtype=float)
        x = (x - self._mean) / self._std   # normalization hidden inside
        return x, self.__labels[index]

    def __len__(self):
        return len(self.__labels)

    @property
    def num_samples(self):
        return len(self)

Callers use dataset[i] and receive normalized features. They cannot accidentally skip normalization by reading raw __features without knowing the mangled name—and they should not need to.

Properties for Validated Access

Properties let you expose read-only or validated attributes:

class TrainingRun:
    def __init__(self, learning_rate):
        self.learning_rate = learning_rate  # uses setter below

    @property
    def learning_rate(self):
        return self._learning_rate

    @learning_rate.setter
    def learning_rate(self, value):
        if value <= 0 or value > 1:
            raise ValueError("learning_rate must be in (0, 1]")
        self._learning_rate = float(value)

Invalid hyperparameters fail at assignment time—not three epochs into a silent divergence.

Don’t Leak Mutable Internals

Anti-Pattern — Returning Internal Lists
class BadDataset:
    def __init__(self, labels):
        self._labels = labels

    def get_labels(self):
        return self._labels   # returns reference — caller can mutate!

ds = BadDataset([0, 1, 0])
labels = ds.get_labels()
labels[0] = 99   # corrupts dataset internal state

Fix: Return a copy (return self._labels.copy()) or expose read-only views. Better: don’t expose internals; provide methods like __getitem__.

Encapsulation in PyTorch Modules

nn.Module encapsulates parameters. You call model.parameters() and model.state_dict()—not model._parameters directly in application code. The framework controls registration, device movement, and gradient hooks internally.

Public API

forward, train, eval, parameters, to(device)

Hidden Internals

Hook registration, parameter dict layout, backward graph wiring

Lesson

Good ML libraries expose minimal, stable surfaces and keep complexity private.

Config Objects vs. Global State

Encapsulating experiment configuration in a class beats scattered globals:

class ExperimentConfig:
    def __init__(self, seed, lr, batch_size):
        self._seed = seed
        self._lr = lr
        self._batch_size = batch_size
        self._locked = False

    def lock(self):
        self._locked = True

    def set_lr(self, value):
        if self._locked:
            raise RuntimeError("Config locked after training started")
        self._lr = value

Once training starts, lock() prevents accidental mid-run hyperparameter changes that break experiment comparability.

Common Misconceptions

Misconception 1: “Python can’t do encapsulation because everything is public.”

Reality: Convention, properties, and careful API design provide effective encapsulation. Framework users rarely touch private attributes—and shouldn’t.

Misconception 2: “Getters and setters for every field is always good.”

Reality: Use properties when validation or computed values matter. Simple data containers (dataclasses) can expose fields directly when invariants are weak.

Quick Knowledge Check

  1. Short Answer: Why return a copy of internal lists from getters? Answer: Prevents callers from mutating encapsulated state.
  2. True/False: Double underscore attributes (__x) are impossible to access in Python. Answer: False—name mangling makes it harder, not impossible.
  3. Short Answer: How does a property setter help ML configs? Answer: Validates values (e.g., positive learning rate) at assignment time.
  4. Short Answer: What does encapsulation hide and what does it expose? Answer: It hides internal state and exposes a controlled public interface.
  5. True/False: A single leading underscore (_cache) means “internal; subclasses may use.” Answer: True.
  6. Multiple Choice: PyTorch encapsulation of parameters is typically via: (a) global lists, (b) parameters() / state_dict(), (c) printing weights, (d) CSV export. Answer: (b).
  7. Short Answer: Why lock a config after training starts? Answer: Prevent accidental mid-run hyperparameter changes that break experiment comparability.
  8. True/False: Every field should always have a getter and setter. Answer: False—use properties when validation or computed values matter.
  9. Short Answer: What should a dataset class enforce internally? Answer: Invariants such as label ranges, feature shapes, and matching lengths.
  10. Multiple Choice: Returning an internal list from a getter without copying: (a) is safest, (b) can leak mutable state, (c) encrypts data, (d) freezes tensors. Answer: (b).

Key Takeaways

  • Encapsulation hides internal state; callers use a controlled public interface.
  • Python uses naming conventions and @property for access control.
  • Datasets and models should enforce invariants internally (shapes, normalization, valid labels).
  • Never leak mutable internal references—return copies or expose behavior via methods.
  • Frameworks like PyTorch encapsulate parameter management behind parameters() and state_dict().
Trainer Guide

Hands-on: Give students a dataset class that returns self.__labels directly. Have them mutate the list from outside, then fix it with a copy or a property. Discuss when dataclasses can expose fields versus when validation setters are required.

Bridge: Point to PyTorch state_dict() as production encapsulation—callers save and load weights without touching private buffers.

What’s Next Continue to Abstraction—the module capstone that ties classes, inheritance, polymorphism, and encapsulation into reusable ML architecture patterns.