2Foundational Skills 2.1Python Basics
2.1.2 Object-Oriented Programming, Classes, and Inheritance
Learn object-oriented Python with one main course, then take a short tour of the classes, methods and inheritance you will meet in scikit-learn and PyTorch.
Why learn object-oriented programming?
In Python Fundamentals, we worked with lists, strings and library functions. That is enough to write plenty of useful Python. But the machine-learning libraries we will use next are built around objects and classes.
You will not write large programs full of classes at IOAI. You will, however, read and complete small classes all the time:
- In scikit-learn, every model is an object.
KNeighborsClassifier(n_neighbors=3)creates one,model.fit(X, y)trains it, andmodel.predict(X)uses it. - In PyTorch, we describe a neural network with a class that inherits from
nn.Module. We often describe a dataset with a class containing__len__and__getitem__.
So our goal is modest: you should be able to read those patterns, change them and write a small class of your own. The rest of object-oriented programming can wait.
What you need to know
By the end of this module, you should be comfortable with these ideas:
| Idea | In one line |
|---|---|
| Objects and classes | A class describes a kind of value; an object is one value made from it. |
| Attributes | Store data inside an object, such as a student's name or a model's learned coefficients. |
| Methods | Put functions inside a class and call them with a dot. |
self and __init__ |
Set up each object and work with its own data. |
| Special methods | Let Python operations such as len(obj) and obj[i] work with your class. |
| Inheritance | Build a child class from a parent class and reuse its behaviour. |
super() |
Run the parent class's setup from a child class. |
That list is enough for the scikit-learn and PyTorch code in this guide.
Learn it properly
This page gives you a guided tour, but a course gives you the practice that makes the ideas stick. The tour will help you recognise a class; the course is where you learn to write one without being led line by line.
Pick one main course
As with Python Fundamentals, do not collect five resources and half-finish all of them. Pick one course, type the examples yourself and complete its exercises.
Our pick: CS50P week 8
Start with Harvard's CS50P week 8: Object-Oriented Programming. It gives you a lecture, written notes and a problem set in one place, and it continues naturally from the CS50P material recommended in Python Fundamentals.
Focus on classes, inheritance and operator overloading. Operator overloading is where you meet special methods such as __add__. Decorators, class methods and static methods are useful, but you do not need to master them yet.
After the lesson, try Problem Set 8. Watching the lecture may make classes look obvious; writing one from a blank file tells you whether self, attributes and methods have actually clicked.
If you prefer something else
- Short videos: watch Corey Schafer's Classes and Instances, then Inheritance. The same example continues across both videos.
- A written tutorial: use Real Python's Object-Oriented Programming in Python. It is longer than you need, so concentrate on defining classes, instance attributes, methods and inheritance.
- Official reference: read sections 9.3 and 9.5 of the Python tutorial. The official tutorial is especially useful when you already know the idea and want to check the exact syntax.
A quick tour
The rest of this page shows what the important ideas look like in code. You do not need to learn OOP from these snippets alone. Go through your chosen course first, then return here and check that you can explain each example and change it yourself.
Objects you already use
You have already written code like this:
scores = [72, 85, 90]
scores.append(64)
print(type(scores))
# <class 'list'>
print("ioai".upper())
# IOAI
scores is an object: a value that holds some data (the numbers) and comes with things it can do (append, sort, and so on). The things it can do are called methods, and you call them with a dot. The type of the object, list, is its class.
Object-oriented programming (OOP) means writing our own classes. A class describes a kind of object, and every object we create from it is an instance of that class.
Your first class
class Student:
def __init__(self, name, scores):
self.name = name
self.scores = scores
def average(self):
return round(sum(self.scores) / len(self.scores), 1)
asha = Student("Asha", [72, 85, 90])
ravi = Student("Ravi", [60, 75])
print(asha.name, asha.average())
# Asha 82.3
print(ravi.name, ravi.average())
# Ravi 67.5
Let us read it line by line:
class Student:starts the blueprint. Class names are written in CapitalisedWords by convention.__init__is a special method that runs automatically when we make a new object, as inStudent("Asha", [72, 85, 90]). Its job is to set the object up.selfis the object itself.self.name = namestores the name inside this particular student. Values stored like this are called attributes.averageis an ordinary method. It also takesself, so it can read that student's own scores.
Each object keeps its own data. If we change Ravi's scores, Asha's scores stay untouched:
ravi.scores.append(95)
print(ravi.average(), asha.average())
# 76.7 82.3
fit and predict
Now we can make a tiny "model" with the same shape as a scikit-learn model. It learns the mean of the training values and predicts that number every time:
class MeanPredictor:
def fit(self, values):
self.value_ = sum(values) / len(values)
return self
def predict(self, how_many):
return [self.value_] * how_many
model = MeanPredictor()
model.fit([3, 5, 10])
print(model.value_)
# 6.0
print(model.predict(2))
# [6.0, 6.0]
fit creates the attribute value_ from the data, and predict uses it later. This is exactly how scikit-learn works: whatever a model learns during fit is stored as an attribute whose name ends in an underscore, like coef_ in linear regression (see Fitted attributes). scikit-learn even has this exact model:
from sklearn.dummy import DummyRegressor
reg = DummyRegressor(strategy="mean")
reg.fit([[0], [1], [2]], [3, 5, 10])
print(reg.predict([[7], [8]]))
# [6. 6.]
It prints [6. 6.] because scikit-learn returns a NumPy array rather than a list. A model this simple is called a baseline, and you will use one in every task to check that a real model is actually better (Terminology explains why).
Special methods
Methods whose names start and end with two underscores, like __init__, are special methods (people also call them "dunder" methods, for "double underscore"). Python calls them for you in certain situations. Two of them matter a lot:
__len__runs when you calllen(obj).__getitem__runs when you writeobj[i].
class ScoreData:
def __init__(self, names, scores):
self.names = names
self.scores = scores
def __len__(self):
return len(self.names)
def __getitem__(self, i):
return self.names[i], self.scores[i]
data = ScoreData(["Asha", "Ravi", "Mei"], [90, 75, 82])
print(len(data))
# 3
print(data[0])
# ('Asha', 90)
Keep this example in mind. Later, we will see that a PyTorch Dataset follows exactly this pattern: it knows how many samples it has and how to return sample number i.
Inheritance
Suppose we also want a model that predicts the median. Its predict method would be identical; only fit changes. Instead of copying the shared code, we can put it in a parent class and let two child classes inherit it:
class ConstantPredictor:
def predict(self, how_many):
return [self.value_] * how_many
class MeanPredictor(ConstantPredictor):
def fit(self, values):
self.value_ = sum(values) / len(values)
return self
class MedianPredictor(ConstantPredictor):
def fit(self, values):
ordered = sorted(values)
self.value_ = ordered[len(ordered) // 2]
return self
mean_model = MeanPredictor().fit([3, 5, 10])
median_model = MedianPredictor().fit([3, 5, 10])
print(mean_model.predict(2), median_model.predict(2))
# [6.0, 6.0] [5, 5]
print(isinstance(median_model, ConstantPredictor))
# True
class MeanPredictor(ConstantPredictor): means "a MeanPredictor is a kind of ConstantPredictor". It gets predict for free and adds its own fit. (This median is only right for an odd number of values, which is fine for the example.)
super()
When the parent class has an __init__ of its own, the child usually needs to run it too. super() lets us reach the parent, so super().__init__(...) runs the parent's setup before the child adds anything:
class Table:
def __init__(self, rows):
self.rows = rows
def __len__(self):
return len(self.rows)
class LabelledTable(Table):
def __init__(self, rows, labels):
super().__init__(rows)
self.labels = labels
def __getitem__(self, i):
return self.rows[i], self.labels[i]
t = LabelledTable([[5.1, 3.5], [6.2, 2.9]], ["setosa", "versicolor"])
print(len(t), t[1])
# 2 ([6.2, 2.9], 'versicolor')
LabelledTable stores the rows through Table's __init__, inherits __len__, and adds labels and __getitem__.
What this looks like in PyTorch
We now have every Python idea used by the two common PyTorch patterns. You do not need to understand the network itself yet; for now, just recognise the shape:
import torch
from torch import nn
class TinyNet(nn.Module):
def __init__(self):
super().__init__()
self.layer = nn.Linear(3, 1)
def forward(self, x):
return self.layer(x)
net = TinyNet()
x = torch.ones(2, 3)
print(net(x).shape)
# torch.Size([2, 1])
TinyNet(nn.Module): your network inherits from PyTorch'snn.Module.super().__init__(): runsnn.Module's own setup first. Forget it and PyTorch stops with an error (see the table below).self.layer = ...: the network's parts are stored as attributes, just likeself.nameinStudent.forward: the method that says what the network does with an input. Callingnet(x)runs it.
A dataset looks almost the same as ScoreData, with one important addition on the first line:
from torch.utils.data import Dataset
class ScoreDataset(Dataset):
def __init__(self, names, scores):
self.names = names
self.scores = scores
def __len__(self):
return len(self.names)
def __getitem__(self, i):
return self.names[i], self.scores[i]
ds = ScoreDataset(["Asha", "Ravi", "Mei"], [90, 75, 82])
print(len(ds), ds[2])
# 3 ('Mei', 82)
Why inherit from Dataset? It is PyTorch's way of saying "this class is a dataset". PyTorch's tools, such as the DataLoader that later feeds your data to a network in batches, are built to work with Dataset objects. They only ever ask a dataset two things, len(ds) and ds[i], which is exactly why the two methods we write are __len__ and __getitem__.
We will build real versions in Dataset and DataLoader and A Simple MLP in PyTorch. You do not need PyTorch installed for this module; the Python ideas are what matter here.
Common mistakes
Classes introduce a few mistakes that almost everyone makes at first. Each message below is exactly what Python prints, so learning to recognise it will save you time.
| Mistake | What Python says |
|---|---|
Forgot self in the method's brackets: def average(): |
TypeError: Student.average() takes 0 positional arguments but 1 was given |
Forgot self. when using an attribute: sum(scores) |
NameError: name 'scores' is not defined. Did you mean: 'self.scores'? |
Called predict before fit |
AttributeError: 'MeanPredictor' object has no attribute 'value_' |
Forgot the brackets when making the object: model = MeanPredictor |
TypeError: MeanPredictor.fit() missing 1 required positional argument: 'values' |
Forgot super().__init__() in a PyTorch model |
AttributeError: cannot assign module before Module.__init__() call |
Notice how helpful the second message is: Python even suggests self.scores as the fix.
How to practise
- Type the classes yourself. Class syntax is easy to recognise and surprisingly easy to forget when you have to begin from a blank cell.
- Make two objects. Give them different attributes and change one. This is the quickest way to see that each instance keeps its own state.
- Predict before running. Before each
print, say what you expect. If Python disagrees, step through the code and find the point where your picture was wrong. - Change one part at a time. Add a method, rename an attribute or make a child class override a parent method.
- Explain the pattern aloud. You should be able to say what
self,__init__, inheritance andsuper()are doing without relying on the exact example.
Check yourself
Try each exercise before opening its answer. If one feels difficult, return to the matching part of your main course and then try again from a blank cell.
If these exercises felt comfortable, you know enough OOP to continue. You do not need to memorise every special method or learn elaborate class design before moving on.
What comes next
- Simple Python Problems: practise everything so far on small problems.
- Jupyter Notebook Interface and Basics: the notebook format every IOAI task uses.
Resources
| Source | Title | Why read it |
|---|---|---|
| Harvard | CS50P, week 8: Object-Oriented Programming | Our recommended main course. It includes a lecture, written notes and a problem set. |
| Corey Schafer | Python OOP Tutorial 1: Classes and Instances | A clear, short video on __init__, self and methods. |
| Corey Schafer | Python OOP Tutorial 4: Inheritance | Subclasses and super(), with the same example continued. |
| Real Python | Object-Oriented Programming (OOP) in Python | A long written tutorial, if you prefer reading. |
| Python.org | The Python Tutorial, chapter 9: Classes | Sections 9.3 (A First Look at Classes) and 9.5 (Inheritance). |
| PyTorch | Build the Neural Network | Where you will meet nn.Module classes for real. Read it later, in the deep learning part. |
Practice problems
| Solved | Source | Problem | Difficulty | Tags |
|---|---|---|---|---|
| CS50P | Problem Set 8 | Medium | classes |
Finished this module? Mark it done.
Comments
Stuck on something, spotted a mistake, or found a resource that helped you? Tell us below.
Loading comments