Unit 5: Classes and objects; Object oriented programming terminology

INT108 — Python Programming 5 min read

I. Orientation: Object-Oriented Programming in Python

OOP organises code around objects — bundles of data and behaviour — rather than sequences of instructions. Python (introduced 1991) supports OOP natively: everything in Python is an object, and the class keyword lets you define your own types. The paradigm rests on four pillars that every later section returns to.

  • Encapsulation: data and the methods that act on it are packaged inside a class; internal state is accessed through a defined interface
  • Inheritance: a new class can derive attributes and methods from an existing one, enabling code reuse
  • Polymorphism: different classes can expose the same method name with different behaviour; calling code need not know the concrete type
  • Abstraction: implementation details are hidden; users interact with a simplified interface

II. Creating Classes and Instance Objects

A. Creating classes

A class is a blueprint for objects, defined with the class keyword and an optional docstring.

  • Syntax: class ClassName: followed by an indented body
  • __init__ constructor: called automatically when an instance is created; receives self (the new instance) plus any initialisation arguments
  • self parameter: every instance method must declare self as its first parameter; Python passes the calling object automatically
PYTHON
class Dog:
    species = "Canis lupus familiaris"   # class attribute

    def __init__(self, name, age):
        self.name = name   # instance attribute
        self.age = age

    def bark(self):
        return f"{self.name} says woof!"

B. Creating instance objects

An instance is a concrete realisation of a class, created by calling the class like a function.

  • Instantiation: obj = ClassName(args) triggers __init__
  • Multiple instances: each holds its own copy of instance attributes; class attributes are shared
PYTHON
d1 = Dog("Rex", 3)
d2 = Dog("Bella", 5)

C. Accessing attributes

Attributes and methods are reached via dot notation on the instance or class.

  • Instance attribute read/write: d1.name"Rex"; d1.age = 4 updates in place
  • Method call: d1.bark()"Rex says woof!"
  • Class attribute access: Dog.species or d1.species; assigning d1.species = "x" creates a shadow instance attribute and leaves the class attribute unchanged
  • __dict__: d1.__dict__ returns {'name': 'Rex', 'age': 3} — only instance attributes

III. Class Inheritance

A. Definition and purpose

Inheritance lets a child class acquire all attributes and methods of a parent class, then extend or specialise them.

  • Syntax: class Child(Parent):
  • super(): returns a proxy to the parent; used inside __init__ to call the parent constructor
PYTHON
class Animal:
    def __init__(self, name):
        self.name = name
    def speak(self):
        return "..."

class Cat(Animal):
    def __init__(self, name, indoor):
        super().__init__(name)
        self.indoor = indoor
  • isinstance(obj, Class): returns True if obj is an instance of Class or any subclass
  • issubclass(Child, Parent): returns True if Child derives from Parent
  • Multiple inheritance: class C(A, B): — Python resolves method lookup via the MRO (Method Resolution Order), inspectable with C.__mro__

B. Overriding Methods

A subclass redefines a method inherited from the parent to change its behaviour.

  • Mechanism: declare a method with the same name in the child class; Python finds the child's version first via MRO
  • Calling the parent version: use super().method_name() inside the override to extend rather than replace
PYTHON
class Cat(Animal):
    def speak(self):          # overrides Animal.speak
        return f"{self.name} says meow"
  • __str__ and __repr__: commonly overridden dunder methods; __str__ controls print(obj), __repr__ controls the developer representation
  • Practical rule: override when the child's behaviour is specialised, call super() when the parent logic is still needed as a base

IV. Data Hiding

A. Definition and convention

Data hiding restricts direct access to an object's internal state, enforcing interaction through methods.

  • Single underscore _attr: convention — signals "internal use"; still accessible from outside but treated as private by agreement
  • Double underscore __attr: triggers name mangling; Python renames it to _ClassName__attr, making accidental external access harder (not impossible)
PYTHON
class BankAccount:
    def __init__(self, balance):
        self.__balance = balance      # mangled to _BankAccount__balance

    def deposit(self, amount):
        self.__balance += amount

    def get_balance(self):
        return self.__balance
  • Access via mangled name: acc._BankAccount__balance still works — Python does not enforce true private access
  • Properties (@property): the idiomatic way to expose controlled read/write access to hidden attributes without breaking the attribute-access syntax
PYTHON
@property
def balance(self):
    return self.__balance

V. Function Overloading

A. Definition and Python's approach

Function overloading means defining multiple versions of a function with different parameter signatures; Python does not support it natively in the classical sense.

  • Python's constraint: only the last definition of a function name survives; earlier definitions are silently replaced
  • Default arguments: the primary substitute — a single method covers multiple call signatures
PYTHON
class Calculator:
    def add(self, a, b=0, c=0):
        return a + b + c
# add(1), add(1,2), add(1,2,3) all work
  • *args / **kwargs: accept variable numbers of positional or keyword arguments, simulating overloading for arbitrary arities
PYTHON
def add(self, *args):
    return sum(args)
  • functools.singledispatch: (Python 3.4+) decorator that dispatches to different implementations based on the type of the first argument — the closest Python comes to true overloading
PYTHON
from functools import singledispatch

@singledispatch
def process(arg):
    raise NotImplementedError

@process.register(int)
def _(arg):
    return arg * 2

@process.register(str)
def _(arg):
    return arg.upper()
  • Operator overloading: a related concept — dunder methods (__add__, __mul__, __eq__, etc.) let user-defined classes respond to built-in operators, e.g. __add__ is called when + is used on an instance