Classes, Objects, and Program StructureIntroductionObject-oriented programming (OOP) is a way of designing software around objects that combine data with the operations that act on that data. In artificial intelligence and machine learning systems, OOP supports the organization of datasets, models, training procedures, evaluation metrics, and services into reusable and maintainable components. A well-designed class can represent a data point, a neural-network layer, a model configuration, or an experiment. This chapter introduces the structural foundations of OOP: classes, objects, fields, methods, constructors, properties, access control, class and instance members, modules, packages, namespaces, and object initialization. Python is used for the main examples because it is widely used in AI and ML. Some principles are language-independent, while Python-specific behavior is identified explicitly. Learning ObjectivesBy the end of this chapter, you should be able to: - Explain the relationship between classes and objects.
- Design classes with appropriate fields, methods, constructors, and properties.
- Distinguish instance members from class members.
- Apply encapsulation and access conventions.
- Create and initialize objects safely.
- Organize programs using modules, packages, and namespaces.
- Analyze design choices involving mutability, validation, and responsibility.
- Apply these principles to a small AI/ML-related system.
1. Classes and ObjectsA class is a definition or blueprint that describes the data and behavior shared by a group of objects. An object is a concrete runtime instance of a class. For example, a class named `Dataset` may define a name, number of rows, and methods for validation. A particular dataset, such as a customer-churn dataset, is an object of that class. A class commonly contains: - Fields or attributes, which represent state.
- Methods, which represent behavior.
- Constructors, which establish a valid initial state.
- Properties, which provide controlled access to data.
- Class members, which belong to the class rather than to one object.
```python class Dataset: def __init__(self, name, rows): self.name = name self.rows = rows def describe(self): return f"{self.name}: {self.rows} rows" training_data = Dataset("training", 10000) print(training_data.describe()) ``` `Dataset` is the class. `training_data` is an object. The call to `Dataset(...)` creates the object and invokes `__init__`, Python’s initializer method. `name` and `rows` are instance attributes because each object can contain different values. `describe` is an instance method because it operates on a particular object. Identity, State, and BehaviorAn object can be understood through three related properties: 1. Identity: what distinguishes it from other objects at runtime. 2. State: the values currently stored by the object. 3. Behavior: the operations the object can perform. Two objects may have equal state but remain different objects. For example, two `Dataset("training", 10000)` objects may contain equivalent data while having separate identities and independent lifetimes. 2. Fields and MethodsFields and AttributesA field stores information associated with an object or class. Python generally refers to such stored values as attributes. Instance attributes are usually created inside `__init__`: ```python class Student: def __init__(self, student_id, name): self.student_id = student_id self.name = name self.completed_courses = [] ``` Each `Student` object receives its own `student_id`, `name`, and `completed_courses`. A frequent error is placing a mutable list at class level when each object should have a separate list: ```python class IncorrectStudent: completed_courses = [] # shared by every instance ``` The correct design creates the list during initialization: ```python class CorrectStudent: def __init__(self): self.completed_courses = [] ``` MethodsA method is a function defined inside a class. In Python, an instance method normally receives `self` as its first parameter. `self` refers to the object on which the method was called. ```python class Counter: def __init__(self): self.value = 0 def increment(self): self.value += 1 ``` The expression `counter.increment()` is conceptually similar to calling `Counter.increment(counter)`. The explicit `self` parameter is part of Python syntax; it is not passed by the programmer in the usual method-call notation. Methods should usually express operations that are meaningful for the class. A `Model` may have `predict`, `save`, and `evaluate` methods. It should not normally contain unrelated operations such as formatting an entire user interface. 3. Constructors and Object InitializationA constructor is a special operation used to create and initialize an object. Different languages use different syntax. In Python, `__new__` technically creates an instance and `__init__` initializes it, but most application-level code treats `__init__` as the constructor. ```python class ModelConfig: def __init__(self, model_name, learning_rate=0.001): self.model_name = model_name self.learning_rate = learning_rate ``` The parameter `learning_rate` has a default value. Therefore, both of the following calls are valid: ```python config_a = ModelConfig("linear_regression") config_b = ModelConfig("neural_network", 0.0001) ``` A constructor should establish a valid and predictable state. It should validate values when invalid input could cause later failures: ```python class Batch: def __init__(self, samples): if samples <= 0: raise ValueError("samples must be positive") self.samples = samples ``` A useful design principle is fail early: reject invalid values at the boundary where they enter the object rather than allowing a corrupt object to circulate through a system. Initialization Versus AssignmentInitialization establishes an object’s initial invariant. An invariant is a condition that should remain true whenever the object is usable. For `Batch`, the invariant is that `samples` is positive. Assignment merely changes a value and does not automatically guarantee that the new value is valid. This distinction motivates properties and validation methods. 4. Encapsulation and Access ControlEncapsulation means combining data and behavior while controlling how internal state is accessed or changed. Encapsulation reduces accidental interference and allows the implementation of a class to change without forcing changes throughout the program. Languages differ in their access-control mechanisms: - Java and C# provide explicit `public`, `private`, and `protected` modifiers.
- Python uses naming conventions and properties rather than strict private fields in ordinary code.
In Python: - `name` is public by convention.
- `_name` means internal or protected-by-convention; external code should normally avoid direct use.
- `__name` triggers name mangling, making accidental access more difficult, but it is not absolute security.
```python class Account: def __init__(self, owner, balance): self.owner = owner self._balance = balance def deposit(self, amount): if amount <= 0: raise ValueError("amount must be positive") self._balance += amount def get_balance(self): return self._balance ``` The class controls how the balance changes. In a financial or production system, stronger access control and transactional safeguards would also be required. PropertiesA property gives attribute-like syntax while executing methods behind the scenes. It is useful for validation, computed values, compatibility, and controlled mutation. ```python class Temperature: def __init__(self, celsius): self.celsius = celsius @property def celsius(self): return self._celsius @celsius.setter def celsius(self, value): if value < -273.15: raise ValueError("temperature cannot be below absolute zero") self._celsius = value ``` The caller writes `temperature.celsius = 20`, but the setter validates the value. A read-only computed property can be defined without a setter: ```python class Rectangle: def __init__(self, width, height): self.width = width self.height = height @property def area(self): return self.width * self.height ``` Here, `area` is derived from current dimensions and should not be assigned independently. This prevents inconsistent states such as a rectangle whose stored area does not match its width and height. 5. Class Members and Instance MembersAn instance member belongs to one object. A class member belongs to the class and is shared conceptually among its instances. ```python class Experiment: platform = "Python" # class attribute def __init__(self, name): self.name = name # instance attribute ``` `Experiment.platform` is available through the class. An instance can also read it if it does not have an attribute with the same name. However, assigning `experiment.platform = "Other"` creates or changes an instance attribute rather than changing the class attribute. This distinction can produce subtle bugs. Class methods receive `cls` and are marked with `@classmethod`: ```python class Model: version = "1.0" def __init__(self, name): self.name = name @classmethod def from_default(cls): return cls("default-model") ``` A class method is appropriate for alternative constructors or operations involving class-wide state. A static method receives neither `self` nor `cls`: ```python class Metrics: @staticmethod def within_range(value, lower, upper): return lower <= value <= upper ``` A static method is placed inside a class because it is conceptually related to the class, not because it needs object state. If no conceptual relationship exists, a module-level function may be clearer. Shared State and ConcurrencyClass-level mutable data is shared. This can be useful for immutable configuration or counters, but it can also create hidden coupling. In multithreaded or distributed applications, updates to shared state may require synchronization or a different architecture. For AI systems running parallel experiments, storing experiment results as a class-level list can accidentally mix results from unrelated runs. 6. Modules, Packages, and NamespacesAs programs grow, placing every class in one file makes maintenance difficult. A module is typically a Python file containing definitions. A package is a directory structure containing related modules, often identified by an `__init__.py` file or supported as a namespace package. Example structure: ```text ml_project/ app.py data/ __init__.py dataset.py models/ __init__.py linear.py ``` A module can define a class: ```python models/linear.pyclass LinearModel: def __init__(self, coefficient): self.coefficient = coefficient ``` It can then be imported: ```python from models.linear import LinearModel model = LinearModel(2.5) ``` A namespace is a context that maps names to objects. Modules provide namespaces, which prevents unrelated definitions from colliding. The names `data.dataset.Dataset` and `evaluation.Dataset` can coexist because they belong to different namespaces. Useful import styles include: ```python import models.linear from models.linear import LinearModel import models.linear as linear ``` The statement `from module import *` is generally discouraged because it hides the origin of names and can cause collisions. Explicit imports improve readability and tooling support. Package Design PrinciplesA package should group components with a coherent responsibility. In an AI application, possible packages include `data`, `features`, `models`, `training`, and `evaluation`. Packages should avoid unnecessary circular dependencies. For example, a low-level data module should not depend on a high-level web interface merely to format an error message. A good dependency direction is often: ```text data -> features -> models -> training/evaluation -> application ``` The exact structure depends on the project, but dependencies should be intentional and documented. 7. Object Creation, References, and MutabilityVariables in Python hold references to objects rather than containing object values directly. Consider: ```python first = [1, 2] second = first second.append(3) ``` Both names refer to the same list, so `first` becomes `[1, 2, 3]`. By contrast, constructing a new list creates an independent object: ```python third = list(first) ``` The distinction matters for class objects. Passing an object to a function does not automatically copy it. If the function mutates the object, the caller may observe the change. This behavior is especially significant for datasets, parameter dictionaries, and model configuration objects. Immutable values such as integers, strings, and tuples cannot be modified in place. Mutable objects such as lists, dictionaries, and most user-defined objects can be changed. A class design should document whether methods mutate the object or return a new object. 8. A Design Process for ClassesA practical class-design process is: 1. Identify the responsibility of the class. 2. Identify the information it must own. 3. State valid ranges and invariants. 4. Define public operations. 5. Hide implementation details that callers should not manage. 6. Decide which values are instance-specific and which are class-wide. 7. Organize related classes into modules and packages. 8. Test construction, normal behavior, invalid input, and boundary cases. A class should generally have a focused responsibility. A class called `TrainingExperiment` may coordinate data, model, and evaluation, but it should not also parse command-line arguments, render charts, and manage database connections unless those responsibilities are deliberately designed. Worked Example: A Validated Model Configuration```python class ModelConfig: supported_optimizers = {"sgd", "adam"} def __init__(self, name, learning_rate=0.001, optimizer="adam"): self.name = name self.learning_rate = learning_rate self.optimizer = optimizer @property def learning_rate(self): return self._learning_rate @learning_rate.setter def learning_rate(self, value): if not isinstance(value, (int, float)) or value <= 0: raise ValueError("learning_rate must be a positive number") self._learning_rate = float(value) @property def optimizer(self): return self._optimizer @optimizer.setter def optimizer(self, value): if value not in self.supported_optimizers: raise ValueError("unsupported optimizer") self._optimizer = value def summary(self): return { "name": self.name, "learning_rate": self.learning_rate, "optimizer": self.optimizer } ``` This class demonstrates instance state (`name`, learning rate, optimizer), class state (`supported_optimizers`), constructor defaults, properties, validation, and a behavior method. The class does not train a model; that responsibility belongs elsewhere. This separation improves testing and reuse. 9. Applied Case: Organizing a Small Image-Classification SystemBackgroundThis illustrative case concerns a university team developing an image-classification prototype. The initial program consists of one large script that loads files, preprocesses images, trains a model, prints metrics, and saves results. ProblemThe script has several weaknesses: - Configuration values are scattered across functions.
- Any function can modify the training data structure.
- A second experiment overwrites results from the first.
- Imports become confusing as new model types are added.
- Invalid learning rates are discovered only after training begins.
AnalysisThe system contains at least four conceptual responsibilities: dataset representation, model configuration, experiment execution, and metric reporting. These responsibilities should not be represented by unrelated global variables. Each object should own the information it is responsible for and expose a small, clear interface. Approach and SolutionThe team creates: - `data/dataset.py` containing a `Dataset` class.
- `models/config.py` containing `ModelConfig`.
- `evaluation/metrics.py` containing metric functions.
- `experiments/run.py` containing an `Experiment` class.
The `Dataset` object validates that its samples and labels have compatible lengths. `ModelConfig` validates the learning rate and optimizer. `Experiment` receives a dataset and configuration through its constructor rather than importing hidden global variables. ```python class Experiment: def __init__(self, dataset, config): self.dataset = dataset self.config = config self._results = None @property def results(self): return self._results def run(self): Training implementation is intentionally separated.self._results = {"status": "completed", "model": self.config.name} return self._results ``` OutcomeThe team can create multiple experiment objects with different configurations. Invalid configurations fail during construction. Tests can use small substitute datasets without changing the training code. Package boundaries make dependencies easier to understand. Learning PointsThe central improvement is not simply the number of classes. It is the deliberate assignment of responsibility, controlled state changes, and explicit dependencies. OOP is valuable when it reduces complexity; creating classes for every small function without a design reason can produce unnecessary complexity. 10. Challenges, Limitations, and Best PracticesOOP is not automatically superior to procedural or functional programming. Classes can introduce inheritance hierarchies, hidden state, and excessive indirection. For simple transformations, a function may be clearer than a class. In AI/ML, numerical libraries often use arrays and functional operations efficiently; wrapping every operation in a custom object may obscure the underlying computation. Recommended practices include: - Keep constructors short and focused on establishing valid state.
- Prefer composition—objects containing other objects—over deep inheritance trees.
- Use properties when validation or computed access is needed.
- Avoid exposing mutable internal collections directly when callers could corrupt invariants.
- Give methods names that communicate side effects, such as `update_weights` or `save`.
- Use type hints and documentation to clarify interfaces.
- Write tests for both valid and invalid construction.
- Keep modules cohesive and imports explicit.
- Avoid mutable class attributes unless sharing is intentional.
- Do not treat Python’s naming conventions as security boundaries.
Ethical and Governance ConsiderationsClass design also affects trustworthy AI. A `Model` class may expose methods for prediction but fail to record the training data version, model version, or evaluation context. For regulated or high-impact applications, objects representing models and experiments should preserve provenance, access permissions, and audit information. Encapsulation can limit unauthorized changes to model configuration, but it cannot replace organizational governance, security controls, bias evaluation, or privacy protection. Contemporary DevelopmentsModern Python projects increasingly use dataclasses, static type checkers, dependency injection, and package-management tools. A dataclass can reduce repetitive initialization code: ```python from dataclasses import dataclass @dataclass class Point: x: float y: float ``` Dataclasses are useful for value-like objects, but they do not automatically enforce domain rules or replace thoughtful class design. Type hints improve documentation and enable tools to detect some errors before execution, but Python remains dynamically executed unless additional validation is introduced. In large ML systems, classes are often combined with pipelines, configuration schemas, dependency-injection frameworks, and service interfaces. The fundamental principles remain unchanged: clear responsibility, valid state, explicit dependencies, and controlled behavior. Chapter SummaryClasses define shared structure and behavior; objects are concrete instances with identity, state, and behavior. Fields represent state, while methods represent operations. Constructors or initializers establish valid initial conditions, and properties provide controlled or computed access to attributes. Encapsulation protects invariants and reduces coupling. Instance members belong to individual objects, whereas class members are associated with the class and may be shared. Static and class methods serve different purposes. Modules and packages organize code, while namespaces prevent name collisions and clarify ownership. Object references and mutability must be understood to avoid accidental shared changes. Effective class design assigns focused responsibilities, validates input early, and uses explicit dependencies. In AI/ML systems, these practices support reusable experiments, auditable configurations, and maintainable software. Key TermsClass: A definition describing data and behavior for a category of objects. Object: A runtime instance of a class. Instance: A concrete object created from a class. Attribute: A named value associated with an object or class. Field: A member used to store state. Method: A function defined within a class. Constructor: An operation used to create or initialize an object. Initializer: In Python, commonly the `__init__` method that establishes object state. Encapsulation: Combining data and behavior while controlling access to internal state. Property: Attribute-like access implemented through methods. Invariant: A condition that should remain true for a valid object. Instance member: A member associated with one particular object. Class member: A member associated with the class and potentially shared by instances. Static method: A method that receives neither instance nor class state automatically. Class method: A method that receives the class, conventionally through `cls`. Module: A file or importable unit containing program definitions. Package: A collection of related modules organized under a common structure. Namespace: A context mapping names to objects. Mutability: The ability of an object to change after creation. Composition: Building a class by combining objects from other classes. Review QuestionsShort Answer Questions1. What is the difference between a class and an object? 2. Why should mutable instance attributes usually be created inside `__init__`? 3. What purpose does a property serve in Python? 4. Distinguish an instance member from a class member. 5. What is the role of a module namespace? Long Answer Questions1. Explain how encapsulation supports maintainability and object invariants. Include a Python example. 2. Describe the responsibilities of constructors, methods, and properties in a well-designed class. 3. Compare instance methods, class methods, and static methods, including suitable use cases. 4. Explain how modules and packages help structure a growing AI/ML application. 5. Discuss the advantages and limitations of using OOP for machine-learning software. Analytical / Application Questions1. Design a `Dataset` class that validates the relationship between samples and labels. Identify its fields, properties, methods, and invariants. 2. A program stores experiment results in a class-level list, causing different users’ results to mix. Analyze the cause and propose a corrected design. 3. Refactor a single script that loads data, trains models, and reports metrics into at least three cohesive classes or modules. Justify the responsibilities and dependency direction you choose. Multiple Choice Questions with AnswersQuestion 1Which statement best describes an object? A. A blueprint containing no runtime state B. A concrete instance of a class C. A directory containing modules D. A name-mapping context only Correct Answer: B. A concrete instance of a class Question 2Where are per-object mutable attributes such as a student’s course list normally initialized in Python? A. Inside the class body only B. Inside a static method C. Inside the initializer, __init__ D. Inside the package’s __init__.py only Correct Answer: C. Inside the initializer, __init__ Question 3What is the primary purpose of a property setter? A. To create a package namespace B. To control and validate assignment to an attribute C. To make every method static D. To copy an object automatically Correct Answer: B. To control and validate assignment to an attribute Question 4What does self normally refer to in a Python instance method? A. The package containing the class B. The class definition itself only C. The object on which the method was called D. The most recently imported module Correct Answer: C. The object on which the method was called Question 5Which member is conceptually shared at the class level? A. An attribute assigned as self.name in __init__ B. A local variable inside an instance method C. A class attribute such as supported_optimizers D. A parameter passed to one method call Correct Answer: C. A class attribute such as supported_optimizers Question 6Which decorator defines a method that receives the class as its first implicit argument? A. @staticmethod B. @classmethod C. @property D. @abstractmethod Correct Answer: B. @classmethod Question 7Why is `from module import *` generally discouraged? A. It prevents all functions from executing B. It always creates circular dependencies C. It can hide name origins and cause collisions D. It converts classes into objects Correct Answer: C. It can hide name origins and cause collisions Question 8If `second = first` where first is a mutable list, what normally happens when second.append(3) is executed? A. Only second changes because assignment copies the list B. Both names observe the changed list C. The list becomes immutable D. A new package is created Correct Answer: B. Both names observe the changed list Question 9Which design principle is demonstrated by rejecting a nonpositive learning rate during object initialization? A. Fail early while establishing a valid state B. Avoiding all validation C. Making every attribute class-level D. Using inheritance instead of composition Correct Answer: A. Fail early while establishing a valid state Question 10What is composition? A. Defining several constructors with identical behavior B. Building a class by combining objects from other classes C. Importing every name from a module D. Making a method private by convention Correct Answer: B. Building a class by combining objects from other classes
|