Object-Oriented Programming: Objects, Inheritance, and Generic Operations

From closure-based state to message passing and data-directed dispatch

From mutation to objects: where does state live?

  • Last time: assignment updates bindings; nonlocal updates enclosing bindings
  • Closure pattern: state captured $+$ procedures that change it
  • Today: classes as a standard interface for that same bundle
  • Driving question: how can one interface support many representations?

Warm-up: the closure-based bank account

def make_account(holder, balance=0):
    """Return a dispatch function representing an account."""
    def deposit(amount):
        nonlocal balance
        balance += amount
        return balance

    def withdraw(amount):
        nonlocal balance
        if amount > balance:
            return 'Insufficient funds'
        balance -= amount
        return balance

    def dispatch(message):
        if message == 'holder':
            return holder
        if message == 'balance':
            return balance
        if message == 'deposit':
            return deposit
        if message == 'withdraw':
            return withdraw
        raise ValueError('Unknown message')

    return dispatch

# Example usage
acc = make_account('Alice', 100)
print(acc('holder'))
print(acc('deposit')(25))
print(acc('withdraw')(70))
print(acc('balance'))
  • State: captured variables $holder$, $balance$
  • Interface: messages like 'deposit', 'withdraw'
  • Mutation: nonlocal balance inside methods
  • Key idea: message passing $=$ object-like behavior

Defining Account: __init__ and self

class Account:
    """A simple bank account."""

    def __init__(self, holder, balance=0):
        self.holder = holder
        self.balance = balance

    def deposit(self, amount):
        self.balance += amount
        return self.balance

    def withdraw(self, amount):
        if amount > self.balance:
            return 'Insufficient funds'
        self.balance -= amount
        return self.balance

# Example usage
acc = Account('Bob', 50)
print(acc.deposit(20))
print(acc.withdraw(10))
print(acc.balance)
  • __init__(self, ...): initialize instance attributes
  • self: the instance being operated on
  • Attributes: acc.holder, acc.balance
  • Methods: acc.deposit(20) changes acc.balance

Methods are functions bound to objects

class Demo:
    def __init__(self, x):
        self.x = x

    def add_to_x(self, amount):
        self.x += amount
        return self.x
obj = Demo(10)

# Two equivalent calls:
print(obj.add_to_x(5))
print(Demo.add_to_x(obj, 5))
  • obj.method(arg) rewrites to Class.method(obj, arg)
  • Bound method: obj.method carries obj as the first argument
  • Names vs values: obj is a name; the instance is a value
  • Dispatch: which function runs depends on the class of the instance

Instance variables vs class variables

class SavingsAccount:
    interest = 0.02  # class variable

    def __init__(self, holder, balance=0):
        self.holder = holder
        self.balance = balance  # instance variable

    def yearly_yield(self):
        return self.balance * self.interest

s1 = SavingsAccount('Ana', 100)
s2 = SavingsAccount('Ben', 100)

print(s1.yearly_yield())

SavingsAccount.interest = 0.05
print(s2.yearly_yield())

s1.interest = 0.08  # shadows the class attribute for s1 only
print(s1.yearly_yield())
print(s2.yearly_yield())
  • Class attribute: SavingsAccount.interest shared default
  • Instance attribute: self.balance unique per instance
  • Assignment: SavingsAccount.interest = ... affects all instances (unless shadowed)
  • Shadowing: s1.interest = ... creates per-instance override

Inheritance: reuse $+$ specialize behavior

class Account:
    def __init__(self, holder, balance=0):
        self.holder = holder
        self.balance = balance
def deposit(self, amount):
        self.balance += amount
        return self.balance
def withdraw(self, amount):
        if amount > self.balance:
            return 'Insufficient funds'
        self.balance -= amount
        return self.balance
class CheckingAccount(Account):
    withdrawal_fee = 1
  • CheckingAccount(Account): inherits methods and attributes
  • Override: withdraw adds fee, then calls super().withdraw(...)
  • New behavior with same interface: still call ch.withdraw(amount)
  • Method lookup chooses the most specific implementation

How Python finds attributes: instance  class  parent

  • Lookup rule: instance attributes first
  • Then: class attributes and methods
  • Then: parent classes (method resolution order)
  • super() = start search at the parent link

The diamond problem: multiple inheritance and MRO

Slide illustration
class A:
    x = "A"

class B(A):
    pass

class C(A):
    x = "C"

class D(B, C):
    pass

print(D.x)       # "C"
print(D.__mro__)
  • Diamond: D inherits from B and C, which both inherit from A
  • MRO for D: [D, B, C, A, object] — $C3$ linearization
  • B does not define x, so Python skips B and finds x in C
  • Rule: search left-to-right across parents, but never visit a class before all its children

SICP twist: one abstract type, many representations

import math
# --- Tag machinery ---
def attach_tag(tag, contents):
    return (tag, contents)
def type_tag(datum):
    return datum[0]
def contents(datum):
    return datum[1]
  • Why not just subclasses? New operations would require editing every representation class; tables flip that dependency
  • Type tag + contents: distinguish 'rect' vs 'polar'
  • Generic selectors: real_part(z) works for both tags
  • Data-directed: add new representation by put(...), not by editing selectors

Dispatch choices: isinstance, methods, or tables?

  • Explicit type checks: if isinstance(x, T): ... (easy, not additive)
  • Method dispatch: x.add(y) (polymorphism, duck typing)
  • Data-directed dispatch: apply_generic(op, x, y) (SICP 2.4/2.5)
  • Message passing: object receives an operation name as data
  • Design goal: new types or new operations with minimal edits

Exit ticket: predict, then explain the mechanism

  • Practice: what prints after ch = CheckingAccount('Eve', 10) then ch.withdrawal_fee = 5 then ch.withdraw(3) then print(ch.balance)?
  • Reflection: when would you prefer tables (apply_generic) over inheritance (super())?

Thank you for watching!

Closing illustration

Like, Share, and Subscribe to Thinking in Math

Powered by SciMigo AI Tutor - https://scimigo.com