There’s a lot to cover in regards to classes and instances. I remember
finding it really hard to wrap my head around the idea when I first
learned about it. So don’t stress if it seems a bit mysterious for a
while.
One way to think about a class and it’s instances is to think of them as
a convenient way to keep some data together with the functions that
operate on that data. For example, you might define a record that holds
data:
A Student record consists of three pieces of data: a name, a student ID,
and a year level (grade 1 to 6). You create a student record:
cartman = ("Eric Cartman", get_new_student_id(), 4)
You might have a bunch of functions that operate on Student records:
def find_classroom(student, time):
# return the classroom the student is supposed to be in
# at that time
def get_grade_average(student):
# return the average grade for the student in each of
# their classes
and more. To use them, you pass the student record to the function:
find_classroom(cartman, "Monday 2pm")
# returns "gym"
Those functions might be scattered around your program, and they might
clash with other functions:
def get_grade_average(teacher):
# return the average grade the teacher gives to their students
Using a class gives a way to bring order to the chaos by bringing all
the relevant Student functions into one place, where they will not
interfere with Teacher functions.
class Student:
def __init__(self, name, starting_year):
# set up the student record
self.name = name
self.id = get_new_student_id()
self.year_level = starting_year
def find_classroom(self, time): ...
def get_grade_average(self):
Notice that we change the parameter “student” to self. This is just a
convention, but a powerful and common one. Now we write:
cartman = Student("Eric Cartman", 4)
cartman.find_classroom("Mon 2pm")
# returns "gym"
and the instance cartman gets passed to the function as “self”. This
happens automatically and you don’t have to worry about how it happens.
We call those functions inside a class methods, and they are logically
equivalent to regular functions, the syntax is just moved around a bit:
find_classroom(cartman, time)
cartman.find_classroom(time)
Notice the special method __init__
. We call that “the constructor”,
although if we want to be really precise, it’s actually an iniatialiser,
there’s another method that is the real constructor. But don’t worry
about that, most of the time you only care about __init__
.
When you call the class as if it were a function:
cartman = Student("Eric Cartman", 4)
Python creates a new student record, an instance of the class, and calls
the constructor __init__
to populate its fields and do any other
preparation needed.
Your constructor is a method like any other method, so you can give it
multiple arguments, with default values, and do anything you like inside
it. There are no hard rules that every class has to do this or do that.
You will see that my Student class has no _value
attribute, and your
Collection class has no name or id attributes.
The only rules are:
-
the __new__
constructor method must return an instance (for advanced
usage only)
-
the __init__
so-called constructor (actually an initialiser) must
populate the instance’s attributes correctly
-
by convention, your methods refer to the instance being worked on
as “self”
(and even those rules are more like guidelines than unbreakable rules).
Remember that original Student record, that looked like three fields?
cartman = ("Eric Cartman", get_new_student_id(), 4)
# Name field, ID field, year level field.
The downside of that is that it is tricky (but not impossible) to refer
to those fields by name. We use numbered items instead:
cartman[0] # returns "Eric Cartman"
By moving to a class, it becomes totally natural to refer to them using
named attributes:
cartman.name # returns "Eric Cartman"
Those named attributes are sometimes called “instance variables”,
although that’s more common in the Java community than Python. In Python
we tend to call them “attributes”, or sometimes “instance attributes”.
There are also class attributes that live on the class, and are
shared by all instances. So we might have:
Student.school # returns "South Park Elementary"
A cool feature of class attributes is that, because they are shared by
all instances, you can do this:
cartman.school # returns "South Park Elementary"
but there’s a catch. If you try to change the name of the school:
cartman.school = "Mr Hat Memorial Elementary School"
it will only change for that one instance, not for the other students.