You are viewing a free preview of this lesson.
Subscribe to unlock all 10 lessons in this course and every other course on LearningBro.
Object-oriented programming is built on one deceptively simple move: instead of keeping data in loose variables and writing free-standing procedures that act on it, you bundle the data and the behaviour together into self-contained units called objects. This lesson develops the code-level mechanics of those units — classes, objects, attributes, methods, instantiation, constructors and the self/this reference — with traced examples in both OCR-style pseudocode and Python.
A note on scope, so you study the right thing in the right place. The Programming Paradigms lesson introduced OOP as one of the four paradigms and named its four pillars. The Object-Oriented Design lesson in the Software Systems course owns the design side — when OOP is appropriate, how to identify classes from a problem, and how to draw class diagrams. This lesson owns the implementation side: how you actually write a class and bring objects to life in code. Where design judgement is needed, cross-link to Object-Oriented Design rather than repeating it here. Throughout, the same small example domains recur — students, books, bank accounts, members — so that you can focus on the mechanics rather than on understanding a new problem each time.
Within H446 2.2, this lesson covers the core constructs of object-oriented code. You should be able to:
self/this reference in identifying the object a method is acting on;The points above paraphrase the specification; no wording is quoted verbatim.
A class is a blueprint — a template that defines what attributes (data) and methods (behaviour) every object of that type will possess. It is a type definition, not a thing that exists at runtime in its own right.
An object is a concrete instance of a class, created while the program runs. Each object carries its own copy of the instance attributes, so two objects of the same class can hold entirely different values.
A helpful analogy: the class is the architect's blueprint for a house; each house actually built from it is an object. The blueprint says "every house has a colour and a number of rooms"; one built house is blue with four rooms, another is white with three. Changing one house does not repaint the other, and the blueprint itself is never lived in.
To see why this is worth the extra machinery, contrast it with the procedural alternative. Procedurally you might store a student's data in a few parallel arrays — names, ages, grades — and write free-standing functions like average_grade(grades, index) that take an index into them. Nothing ties the three arrays together, nothing stops a function from reading names[i] with the wrong i, and adding a new field means editing every function that walks the arrays. The class gathers all of one student's data into a single object and attaches the relevant operations to it, so the data and the code that maintains it travel together and the rest of the program cannot reach past the methods to corrupt the state. That bundling is the essence of the object-oriented mechanics this lesson teaches.
| Term | Definition |
|---|---|
| Class | A template defining the attributes and methods of a type of object. |
| Object | A specific instance of a class, holding its own attribute values. |
| Instantiation | The act of creating an object from a class. |
| Attribute | A variable belonging to an object that stores part of its state. |
| Method | A subroutine defined inside a class that operates on an object's data. |
classDiagram
class Student {
-name: string
-age: int
-grades: list
+get_name() string
+set_name(n: string) void
+add_grade(g: int) void
+get_average_grade() float
}
The diagram shows the class; alice and bob below would be objects built from it, each with its own name, age and grades.
# OCR-style OOP pseudocode
CLASS Student
PRIVATE name: STRING
PRIVATE age: INTEGER
PRIVATE grades: ARRAY OF INTEGER
PUBLIC PROCEDURE new(n: STRING, a: INTEGER)
name = n
age = a
grades = []
END PROCEDURE
PUBLIC FUNCTION getName() RETURNS STRING
RETURN name
END FUNCTION
PUBLIC PROCEDURE setName(n: STRING)
name = n
END PROCEDURE
PUBLIC PROCEDURE addGrade(grade: INTEGER)
grades.append(grade)
END PROCEDURE
PUBLIC FUNCTION getAverageGrade() RETURNS REAL
IF LENGTH(grades) = 0 THEN
RETURN 0
END IF
total = 0
FOR i = 0 TO LENGTH(grades) - 1
total = total + grades[i]
NEXT i
RETURN total / LENGTH(grades)
END FUNCTION
END CLASS
Read the structure carefully, because it is the template OCR expects you to reproduce: CLASS … END CLASS wraps the definition; PRIVATE declarations list the attributes; PROCEDURE new is the constructor; and methods are split into PROCEDURE (no return) and FUNCTION ... RETURNS (returns a value). Notice that inside getAverageGrade the names grades, total and i are used directly — within a class, unqualified attribute names refer to this object's attributes.
class Student:
def __init__(self, name: str, age: int):
self.__name = name # private instance attribute
self.__age = age # private instance attribute
self.__grades = [] # private instance attribute
def get_name(self) -> str:
return self.__name
def set_name(self, name: str):
self.__name = name
def add_grade(self, grade: int):
self.__grades.append(grade)
def get_average_grade(self) -> float:
if len(self.__grades) == 0:
return 0
return sum(self.__grades) / len(self.__grades)
Line for line this mirrors the pseudocode. __init__ is the Python constructor; the __ (double underscore) prefix marks each attribute as private (see Encapsulation and Access for the mechanism); and every method takes self first so it knows which object's data to use.
Instantiation is the process of creating an object from a class. When you instantiate, the constructor runs automatically to set up the new object's attributes.
# Creating (instantiating) two objects from the one class
student1 = Student("Alice", 17)
student2 = Student("Bob", 18)
# Each object owns its own state
print(student1.get_name()) # Alice
print(student2.get_name()) # Bob
# Objects are independent: changing one does not touch the other
student1.add_grade(85)
student2.add_grade(72)
print(student1.get_average_grade()) # 85.0
print(student2.get_average_grade()) # 72.0
Trace it. Student("Alice", 17) triggers __init__ with name="Alice", age=17; the new object's __name becomes "Alice", __age becomes 17, __grades becomes []. A separate object is built for Bob with its own attributes. student1.add_grade(85) appends 85 to student1's __grades only — student2.__grades is untouched, which is why the two averages differ. This independence of per-object state is the whole point of instantiation, and it is the property that distinguishes an object from a global record that everyone shares.
Exam Tip: "Create an instance of" and "instantiate" mean the same thing: call the constructor. Write the class name followed by the constructor's arguments, and (in code) assign the result to a variable.
Attributes are the variables that belong to an object and together make up its state. Most attributes are instance attributes, but a class can also hold class attributes shared by every object.
| Type | Belongs to | Copies | Typical use |
|---|---|---|---|
| Instance attribute | A single object | One per object | The object's own data (self.__name) |
| Class attribute | The class itself | One, shared by all | Counts/constants common to all objects (Student.count) |
class Student:
count = 0 # class attribute: shared by all instances
def __init__(self, name: str):
self.__name = name # instance attribute: unique to each object
Student.count += 1 # update the single shared counter
s1 = Student("Alice")
s2 = Student("Bob")
print(Student.count) # 2 (one shared value across all instances)
print(s1.get_name() if hasattr(s1, "get_name") else s1._Student__name) # Alice
Each Student(...) gives the new object its own __name, but they all increment the one Student.count. After two instantiations the shared counter reads 2. A classic exam discriminator is recognising that a counter of "how many objects exist" must be a class attribute, not an instance attribute — if it were per-object it could never see the others.
Methods are subroutines defined inside a class; they give objects their behaviour and usually read or update the object's attributes.
| Type | Role |
|---|---|
| Instance method | Acts on one object's data; takes self first. |
| Constructor | Runs on creation to initialise attributes (__init__). |
| Getter (accessor) | Returns the value of a private attribute. |
| Setter (mutator) | Updates a private attribute, normally with validation. |
| Static method | Belongs to the class, not any object; uses no instance data. |
class Student:
def __init__(self, name: str, age: int):
self.__name = name
self.set_age(age) # route even the initial value through validation
def get_age(self) -> int: # getter
return self.__age
def set_age(self, age: int): # setter with validation
if age < 0 or age > 150:
raise ValueError("Age must be between 0 and 150")
self.__age = age
s = Student("Alice", 17)
print(s.get_age()) # 17
s.set_age(18) # accepted
print(s.get_age()) # 18
# s.set_age(-3) # would raise ValueError: invalid age rejected
The constructor deliberately calls set_age rather than assigning self.__age directly, so the validation rule lives in exactly one place and applies to the very first value too. Calling set_age(-3) would raise before the bad value could ever be stored — the object cannot be put into an invalid state. The full treatment of why this matters belongs to Encapsulation and Access; here the point is the mechanical pattern of writing getters and setters.
Exam Tip: A setter that merely assigns is barely better than a public attribute. Markers look for validation inside setters as the evidence that you understand controlled access.
A constructor is the special method that runs automatically the moment an object is instantiated. Its job is to put the object into a valid initial state by setting up its attributes.
class Car:
def __init__(self, make: str, model: str, year: int):
self.__make = make
self.__model = model
self.__year = year
self.__speed = 0 # a sensible default, not supplied by the caller
# OCR-style OOP pseudocode
CLASS Car
PRIVATE make: STRING
PRIVATE model: STRING
PRIVATE year: INTEGER
PRIVATE speed: INTEGER
PUBLIC PROCEDURE new(m: STRING, mo: STRING, y: INTEGER)
make = m
model = mo
year = y
speed = 0
END PROCEDURE
END CLASS
__init__ by name.speed = 0) for anything the caller does not provide.The phrase "valid initial state" deserves unpacking, because it is where good constructors earn their keep. Every class carries an implicit set of rules its objects must always obey — a BankAccount balance is never negative, a Member's sessions-attended starts at zero, a Book is not simultaneously available and borrowed. These rules are sometimes called the class invariants. The constructor's real responsibility is not merely to copy parameters into attributes but to establish those invariants from the very first moment the object exists. That is why a careful constructor routes incoming values through the same validating setters that guard later updates:
class BankAccount:
def __init__(self, opening_balance: float):
self.__balance = 0
self.deposit(opening_balance) # validate the opening figure too
def deposit(self, amount: float):
if amount <= 0:
raise ValueError("Deposit must be positive")
self.__balance += amount
If the constructor assigned self.__balance = opening_balance directly, a caller could create an account with a negative balance and the invariant would be broken before any method had a chance to object. By reusing deposit, the rule "balance only ever grows by a positive deposit" holds uniformly — at creation and ever after. Recognising that the constructor is the first line of defence for an object's invariants, not just an attribute-copier, is the kind of insight that separates a mechanical answer from a genuinely understood one.
The self reference (Python) — this in Java and C++ — names the current object: the specific instance on which the method was called. It is how a method knows whose attributes to use.
class Dog:
def __init__(self, name: str):
self.__name = name # 'self' is the object being constructed
def bark(self):
print(f"{self.__name} says Woof!") # 'self' is whoever called bark
rex = Dog("Rex")
bella = Dog("Bella")
rex.bark() # Rex says Woof! (self is rex, so self.__name is "Rex")
bella.bark() # Bella says Woof! (self is bella, so self.__name is "Bella")
When you write rex.bark(), Python silently passes rex as self, so self.__name resolves to "Rex". The same method definition produces different output for bella because self now points at the other object. Without self, a method could not tell two instances apart — every Student would somehow have to share one name, which defeats the purpose of objects.
| Language | Keyword | Notes |
|---|---|---|
| Python | self | Must be the explicit first parameter of every instance method. |
| Java | this | Implicit; used to disambiguate a field from a same-named local/parameter. |
| C++ | this | A pointer to the current object; used like Java's this. |
Exam Tip: In Python, omitting
selffrom a method's parameter list — or forgettingself.when accessing an attribute — is a frequently tested error. In OCR pseudocode the current object is usually implicit, so unqualified attribute names already mean "this object's".
Subscribe to continue reading
Get full access to this lesson and all 10 lessons in this course.