Unit 3: OOP concepts

ECAP776 6 min read

I. Orientation — Object-Oriented Programming in Python

Object-oriented programming (OOP) organizes software around objects that combine state and behavior. Python implements OOP through classes, instances, attributes, and methods; almost every Python value, including an integer, function, or list, is an object belonging to a class.

A. Governing Principle

The central principle of OOP is to model a program as cooperating objects, each responsible for managing related data and operations.

  • Class: A blueprint defining the attributes and methods shared by a category of objects; for example, class Account describes what every account stores and does.
  • Object or instance: A concrete value created from a class; account = Account() creates an instance of Account.
  • State: The data associated with an object, normally stored in instance attributes such as account.balance.
  • Behavior: The operations an object can perform, represented by methods such as account.deposit(500).
  • Identity: Each object has a distinct identity even when its state equals another object’s state; a is b tests whether two names refer to the same object.
  • Class convention: Python class names normally use CapWords, while methods and attributes use snake_case.
  • Object lifecycle:
    • __new__() creates an instance and is inherited from object in ordinary classes.
    • __init__() initializes the newly created instance.
    • Python’s garbage collector reclaims an object when it is no longer reachable.
  • Root class: Classes ultimately inherit behavior from Python’s built-in object class, directly or indirectly.
  • Member access: The dot operator selects an attribute or method, as in student.name or student.display().

II. OOP Features — Structure and Interaction of Objects

Python’s object model supports modular design by placing related responsibilities in classes and allowing objects to interact through well-defined interfaces.

A. OOP features

The principal OOP features explain how classes model entities, hide complexity, reuse behavior, and provide interchangeable interfaces.

  • Classes and objects: A class defines a new type, while its instances hold independent state.
PYTHON
class Rectangle:
    category = "quadrilateral"       # Class attribute

    def __init__(self, width, height):
        self.width = width           # Instance attribute
        self.height = height

    def area(self):
        return self.width * self.height

r1 = Rectangle(4, 3)
r2 = Rectangle(8, 2)

print(r1.area())       # 12
print(r2.area())       # 16
  • self refers to the instance on which a method is called.
  • category belongs to the class and is shared unless shadowed by an instance attribute.
  • width and height belong separately to r1 and r2.
  • Abstraction: A class exposes essential operations while concealing unnecessary implementation details; a caller can use r1.area() without knowing how the multiplication is performed.
  • Encapsulation: Data and the methods operating on it are grouped inside one class; an Account can manage its own balance rather than allowing unrelated code to control it freely.
  • Inheritance: A new class can derive attributes and methods from an existing class; SavingsAccount(Account) represents an “is-a” relationship.
  • Polymorphism: Different object types can respond to the same operation in type-appropriate ways; both Circle.area() and Rectangle.area() may provide an area() interface.
  • Dynamic binding: Python determines the method implementation from the actual object at runtime. If shape refers to a Circle, shape.area() invokes Circle.area().
  • Message passing: Objects communicate through method calls. In account.withdraw(200), the request and argument are sent to the account object.
  • Composition: One object can contain another to represent a “has-a” relationship; a Car might store an Engine object rather than inherit from it.
  • Reusability: A tested class can be instantiated or extended in several programs, reducing duplicate code.
  • Modularity: Each class can represent one coherent responsibility, making faults and changes easier to isolate.

B. Applications and limitations

OOP is most effective when a system contains entities with meaningful state and behavior, but it is not automatically the best design for every program.

  1. Applications:

    • Graphical applications: Classes such as Window, Button, and Menu model interface components with distinct state and event-handling behavior.
    • Business systems: Customer, Invoice, and Payment objects map naturally to domain entities.
    • Simulations and games: Objects such as Player and Vehicle retain changing state across many operations.
    • Frameworks: Web and GUI frameworks use inheritance, composition, and polymorphic callbacks to let developers customize behavior.
  2. Limitations:

    • Unnecessary complexity: Wrapping a short calculation in several classes can be less readable than a function.
    • Mutable state: Many objects changing one another’s attributes can make program behavior difficult to trace.
    • Over-engineering: Deep inheritance hierarchies and excessive abstraction increase coupling rather than reducing it.
    • Design alternative: Procedural or functional styles may suit data transformations, scripts, and stateless computations better; Python allows these styles to coexist with OOP.

III. Encapsulation — Controlling Object State

Encapsulation combines state with the operations responsible for that state and limits dependence on internal representation. Python enforces some boundaries through mechanisms such as properties, while other boundaries depend on naming conventions and programmer discipline.

A. Encapsulation

Encapsulation protects class invariants by directing access through a controlled public interface instead of exposing every implementation detail.

  • Public members: Names such as balance and deposit() are freely accessible and form the class’s normal interface.
  • Protected-name convention: A single leading underscore, as in _balance, signals that a member is intended for internal or subclass use; Python does not prevent external access.
  • Name mangling: An identifier beginning with two underscores, such as __pin, is transformed to a name containing the class name, approximately _ClassName__pin.
    • It reduces accidental clashes in subclasses.
    • It is not a security mechanism because the transformed attribute can still be accessed deliberately.
  • Property: The property mechanism lets attribute-style syntax invoke methods, allowing validation without changing the public interface.
  • Invariant: A condition that should remain true for every valid object; an account may require balance >= 0.
  • Getter: A property method returns controlled information about internal state.
  • Setter: A property method validates or transforms a proposed value before storing it.
  • Information hiding: Callers depend on what an object promises to do, not on whether it stores a value directly, computes it, or retrieves it elsewhere.
PYTHON
class BankAccount:
    def __init__(self, opening_balance=0):
        if opening_balance < 0:
            raise ValueError("Opening balance cannot be negative")
        self._balance = opening_balance

    @property
    def balance(self):
        return self._balance

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("Deposit must be positive")
        self._balance += amount

    def withdraw(self, amount):
        if amount <= 0 or amount > self._balance:
            raise ValueError("Invalid withdrawal")
        self._balance -= amount

account = BankAccount(1000)
account.deposit(250)
account.withdraw(400)
print(account.balance)       # 850
  • Concrete effect: Only deposit() and withdraw() modify _balance, so both operations can preserve the non-negative balance invariant.
  • Read-only interface: Because balance has no @balance.setter, an assignment such as account.balance = 0 raises AttributeError.
  • Exception use: ValueError reports that the supplied amount has an unacceptable value, while leaving the object in its previous valid state.

B. Applications and limitations

Effective encapsulation reduces coupling, although Python intentionally favors cooperative interfaces rather than absolute access restrictions.

  1. Applications:

    • Validation: A Temperature property can reject values below an accepted physical or application-specific boundary.
    • Stable interfaces: An attribute can later become a computed property while callers continue using object.attribute.
    • Maintenance: Internal storage may change from _balance to transaction records without changing deposit() or withdraw().
    • Testing: Tests can target public behavior and invariants rather than fragile implementation details.
  2. Limitations:

    • No strict private access modifier: Python has no Java-style private keyword that makes an attribute completely inaccessible.
    • Property overuse: Trivial getters and setters add little value when no validation, computation, or compatibility requirement exists.
    • False security: Name mangling discourages accidental access but cannot protect secrets from code running in the same process.
    • Subclass constraints: Excessively hidden implementation details can make legitimate extension difficult; documented protected hooks are often preferable.

IV. Inheritance — Reusing and Specializing Behavior

Inheritance creates a new class from one or more existing classes. The derived class receives accessible behavior from its base classes and may add, replace, or extend that behavior.

A. Inheritance

Inheritance models a genuine “is-a” relationship and enables objects from related classes to be used through a common interface.

  • Base class: The class supplying inherited behavior, such as Employee.
  • Derived class: The specialized class, such as Manager(Employee).
  • Method overriding: A derived class defines a method with the same name to replace the inherited implementation.
  • super(): This function delegates a method call according to the method resolution order, commonly allowing a subclass to extend base initialization.
  • Method resolution order (MRO): The ordered sequence in which Python searches classes for an attribute; ClassName.mro() displays that sequence.
  • Subtype polymorphism: Code expecting an Employee-like interface can also operate on a Manager that supplies the required methods.
PYTHON
class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary

    def describe(self):
        return f"{self.name}: employee"

class Manager(Employee):
    def __init__(self, name, salary, department):
        super().__init__(name, salary)
        self.department = department

    def describe(self):
        return f"{self.name}: manager of {self.department}"

worker = Employee("Asha", 40000)
leader = Manager("Ravi", 60000, "Sales")

print(worker.describe())     # Asha: employee
print(leader.describe())     # Ravi: manager of Sales
  • Single inheritance: One derived class has one direct base class, as in Manager(Employee).
  • Multilevel inheritance: A class derives from an already derived class, such as SeniorManager(Manager).
  • Hierarchical inheritance: Several classes derive from one base, such as Manager(Employee) and Developer(Employee).
  • Multiple inheritance: A class has several direct bases, as in SmartDevice(Camera, Phone); Python’s MRO resolves lookup order.
  • Interface checking: isinstance(leader, Employee) returns True, while issubclass(Manager, Employee) also returns True.

B. Applications and limitations

Inheritance is useful for stable subtype relationships, but composition is often safer when behavior merely needs to be assembled.

  1. Applications:

    • Shared implementation: Common initialization and methods remain in one base class instead of being copied.
    • Specialization: Manager adds department while retaining the state supplied by Employee.
    • Framework extension: A user-defined class can override framework methods such as event handlers.
    • Uniform processing: A collection of different subclasses can be processed by calling a shared method such as describe().
  2. Limitations:

    • Tight coupling: A subclass may rely on base-class details, so a base change can break several descendants.
    • Fragile hierarchies: Deep inheritance chains make it difficult to identify where behavior originates.
    • Multiple-inheritance ambiguity: Classes sharing ancestors can create complex initialization paths; cooperative super() calls must follow compatible signatures.
    • Composition preference: If a Car merely uses an Engine, storing an engine object better represents the relationship than declaring Car(Engine).