Object Oriented Programming: Classes and Objects
What a class actually is, how objects hold their own data, and why this way of organising code took over the software industry.
The idea
So far, data and the code that works on it have been separate. Object oriented programming puts them together.
A class is a blueprint. An object is a thing built from that blueprint.
Think of a class as the plan for a house and objects as the actual houses. One plan, many houses, each with its own address, its own occupants and its own front door colour.
class Dog:
def __init__(self, name, breed):
self.name = name
self.breed = breed
def bark(self):
print(f"{self.name} says woof!")
rex = Dog("Rex", "Labrador")
bella = Dog("Bella", "Collie")
rex.bark() # Rex says woof!
bella.bark() # Bella says woof!
print(rex.breed) # LabradorThe parts
class Dog:defines the blueprint. Class names use CapitalCase by convention.__init__is the constructor. It runs automatically whenever a new object is created, and it sets up that object's starting data.selfrefers to this particular object. Every method takes it as its first parameter.self.nameis an attribute, a piece of data belonging to this object.barkis a method, a function belonging to the class.
rex and bella share the same methods but have completely separate data. When rex.bark() runs, self is rex, so self.name is Rex. The same code produces different behaviour because the data is different.
- The class Dog is the blueprint: it says which attributes and methods every dog has.
- Each object made from it has its own attribute values. They share the methods.
- A class is a blueprint, an object is one thing built from it, and self means this particular object.
Building a useful class
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
self.transactions = []
def deposit(self, amount):
if amount <= 0:
return False
self.balance += amount
self.transactions.append(("deposit", amount))
return True
def withdraw(self, amount):
if amount <= 0 or amount > self.balance:
return False
self.balance -= amount
self.transactions.append(("withdraw", amount))
return True
def statement(self):
lines = [f"Account: {self.owner}"]
for kind, amount in self.transactions:
lines.append(f" {kind:<9} {amount:>8.2f}")
lines.append(f"Balance: {self.balance:.2f}")
return "\n".join(lines)
account = BankAccount("Aisha", 100)
account.deposit(50)
account.withdraw(30)
account.withdraw(9999) # refused, returns False
print(account.statement())Notice that the account cannot go overdrawn, because the rule lives inside the class alongside the data it protects. That is the real value of OOP: the object looks after its own correctness.
Special methods
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
return f"({self.x}, {self.y})"
def __eq__(self, other):
return self.x == other.x and self.y == other.y
p = Point(3, 4)
print(p) # (3, 4), because of __str__
print(Point(1, 2) == Point(1, 2)) # True, because of __eq____str__ decides what print() shows. Without it you get something unhelpful like <__main__.Point object at 0x7f...>.
Encapsulation
Encapsulation means bundling data together with the methods that operate on it, and controlling access to that data from outside.
Why it matters
If any part of a program can reach in and change an object's data directly, the object cannot guarantee anything about itself.
account.balance = -5000 # nothing stops thisPrivate attributes by convention
A leading underscore signals that an attribute is internal and should not be touched from outside. Two underscores makes Python actively mangle the name to discourage it.
class Temperature:
def __init__(self, celsius=0):
self._celsius = celsius
def get_celsius(self):
return self._celsius
def set_celsius(self, value):
if value < -273.15:
raise ValueError("Below absolute zero")
self._celsius = value
def get_fahrenheit(self):
return self._celsius * 9 / 5 + 32
t = Temperature(20)
print(t.get_fahrenheit()) # 68.0
t.set_celsius(25)Properties, the Pythonic way
class Temperature:
def __init__(self, celsius=0):
self._celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("Below absolute zero")
self._celsius = value
@property
def fahrenheit(self):
return self._celsius * 9 / 5 + 32
t = Temperature(20)
print(t.celsius) # 20, looks like an attribute
print(t.fahrenheit) # 68.0, calculated on demand
t.celsius = 30 # runs the setter, which validatesThis gives clean syntax with full control. The validation runs every time, but the code using it reads like a simple attribute.
Encapsulation means an object controls its own data, so it can guarantee that data stays valid no matter what the rest of the program does.
- The data is private, inside the object.
- Other code cannot change it directly, but it can ask through a method.
- That keeps the object valid, and lets the inside change without breaking anything that uses it.
Key terms you must be able to define
- Class
- A blueprint defining the attributes and methods that objects of that type will have.
- Object
- A specific instance created from a class, with its own data.
- Instance
- Another word for an object created from a class.
- Attribute
- A piece of data belonging to an object.
- Method
- A function defined inside a class that operates on an object.
- Constructor
- The __init__ method, which runs automatically when an object is created.
- self
- The parameter referring to the particular object a method is operating on.
- Encapsulation
- Bundling data with the methods that act on it, and controlling access from outside.
- Property
- A method that is accessed like an attribute, allowing validation on assignment.
Flashcards
Say the definition out loud before you flip the card. Retrieving it from memory is what builds recall, reading it again is not.
Reach the top grade on this topic
- Explain the difference between a class and an object using a concrete example
- Write a constructor that sets up every attribute the object needs
- Put validation inside the class so the object cannot be put into an invalid state
- Add
__str__to every class so printing an object is useful - Explain encapsulation as the object controlling its own data, not just as hiding
Mistakes that cost marks here
- Forgetting self as the first parameter of every method.
- Forgetting self when referring to an attribute inside a method.
- Confusing a class with an object. The class is the blueprint, the object is built from it.
- Putting validation in the calling code rather than inside the class, so any other caller can bypass it.
- Defining attributes outside __init__, so different objects end up with different attributes.
The idea of objects came from a 1960s language called Simula, written to simulate real world systems such as ships in a harbour. Modelling real things as objects with their own state turned out to be useful for almost everything.
Knowledge check
__init__ do?self refer to?__str__ control?@property decorator allow?Exam-style questions
Five written questions in the style of the real paper. Write a full answer, then let the marker check it against the mark scheme.
- LA class is a template or blueprint defining attributes and methods
- LAn object is a specific instance created from that class
- LGives a valid example such as a Dog class with individual dogs as objects
A class is a blueprint that defines what attributes and methods every object of that type will have, but it does not itself hold any particular data. An object is a specific instance created from that class, with its own values for each attribute. For example, a Dog class might define that every dog has a name and a breed and can bark. Creating rex = Dog("Rex", "Labrador") and bella = Dog("Bella", "Collie") produces two separate objects from that one class, sharing the same methods but each holding its own name and breed.
- LDefines the class with a suitable name
- LDefines __init__ with self, width and height
- LAssigns the parameters to attributes using self
- LDefines a method taking self
- LReturns width multiplied by height
class Rectangle: def __init__(self, width, height): self.width = width self.height = height def area(self): return self.width * self.height r = Rectangle(5, 3) print(r.area()) # 15 The constructor runs automatically when a Rectangle is created and stores the width and height as attributes of that particular object. The area method takes self as its first parameter so that it can access those attributes, and it returns the result rather than printing it, so the value can be used in further calculations.
- LData and the methods that operate on it are bundled together in a class
- LAccess to the data from outside is controlled
- LThe object can therefore validate any change to its data
- LThis prevents other parts of the program putting the object into an invalid state
Encapsulation means bundling an object's data together with the methods that operate on that data inside a single class, and controlling how the data can be accessed from outside. Rather than allowing any part of the program to change an attribute directly, changes are made through methods or properties belonging to the class. This is useful because it means the object can validate every change before accepting it, so a bank account can refuse a withdrawal that would take the balance below zero, and a temperature can refuse a value below absolute zero. Because the rule lives inside the class, it applies automatically everywhere the class is used, and no other part of the program can accidentally or deliberately put the object into a state that should be impossible.
- LIt refers to the particular object the method is being called on
- LIt allows the method to access that object's own attributes
- LDifferent objects therefore produce different results from the same method
The self parameter refers to the specific object on which a method has been called. When rex.bark() is executed, Python passes rex in as self, so inside the method self.name refers to rex's name rather than any other dog's. This is what allows every object created from the same class to share the same method code while behaving differently, because each one has its own set of attribute values. Without self, a method would have no way of knowing whose data it was supposed to be working with.
- LStore the mark in a private attribute with a leading underscore
- LProvide a setter method or property to change it
- LValidate the value inside that method before assigning it
- LReject or raise an error for invalid values
The mark would be stored in a private attribute such as self._mark, with the leading underscore signalling that it is internal and should not be accessed directly from outside the class. Access would then be provided through a property, with a getter that simply returns the value and a setter that checks the new value before accepting it. The setter would test whether the value is between 0 and 100 inclusive and assign it only if so, raising a ValueError otherwise. Because the check lives inside the class, every piece of code that ever sets a mark goes through it automatically, so it is impossible for any part of the program to leave a student object holding an invalid mark.