Lecture Notes 23: Intro to Classes and Objects
What is an Object? (in Python)
Some entities with which we interact are more than just a value.
If you think about the objects you interact with in your everyday life, there are
- simple objects without a "function", like a picture, or a painting, or
- complex ones with some "functionality or purpose", like a microwave, or a swiss-army knife
There are even complex ones that are not entirely physical, like a "College", which is composed of students, teachers, courses, etc.
You can think of
complex entities as those that could be a "collection of values with some structure", or a "collection of values
and methods".
Here, "methods" refers to the capacity to "do things" (for example: teach students, train other teachers, do research, etc).
In Python, we say that an
object is an
instance (one example entity) of one of these complex entities that can have (one or many) values and can "do things".
Abstraction with Encapsulation
When you want to use a microwave to heat a dish, you only need to know:
- how to open and close the door
- how to place the dish
- how much time to indicate
- how to enable the cycle
- (nobody really changes the power level, do they?)
What you don't do
What you don't do is study how the rotation of the dish causes the heating interferrence pattern inside the microwave to affec all areas of your dish.
Nor do you need to understand the programming behind the button interface.
Why?
(Wait; then Click)
Because you don't care.. you just want your damn noodles!
This idea... of hiding away what we don't need to know about and presenting only the information that is relevant in a given context... is called
Abstraction!
The creation of objects that can have:
- complex internal workings, but
- an easy-to use interface
is one of the main putposes of designing and creating objects.
Abstract Data Types
Abstract Data Types, or (ADTs) are examples of complex objects whose behavior and state is controlled by a carefully designed set of operations and an interface into these operations.
Built-in Objects
Python let's you declare and use objects. In fact, we've been using them all along!
Example:
Strings!
Strings hold values and allow you to use their set of member methods by using the dot operator:
my_str = "Hi there, I am Yak, master of the universe"
my_str = my_str.replace("master", "disaster")
print(my_str)
Same deal with
Lists,
Dictionaries,
Files, and others.
Making your own Objects: Classes
If the specific data structure or behavior you need is not provided by Python, you can design and create it yourself!
The class syntax (encapsulation)
1
2
3
4
5
6
7
8 | class Student:
name = "Andy"
grades = [70, 80, 90, 100]
def fun():
print(Student.name + "'s grades are: ", Student.grades)
Student.fun()
|
Activity 0 [2 minutes]:
Run it in the
Python Tutor and explain what is going on.
(We will explain the syntax soon... for now, simply try to guess what is going on)
This is not much use other than for "Packaging different things together"
Since we might want to use more than a single instance of this user-defined object, we are going to design a
template for stamping out these new type of objects.
The way to do this is to define the
template by using the
class keyword.
A
class is like a stencil, with which you can stamp out instances that are like copies of the stencil.
The following is an extremely simple class that allows you to group information about a student's grades in a user-defined object we'll call
Student
The class syntax (template)
The following is the
template version of the Student class that allows you to group information about
different student names and grades object
stamped out of the new Student template class:
1
2
3
4
5
6
7
8
9
10 | class Student:
def __init__(self, par1, par2):
self.name = par1
self.grades = par2
s1 = Student("Amy", [83, 88, 85, 94, 100])
print(s1)
print(s1.name)
print(s1.grades)
|
Activity 1 [2 minutes]:
Run it in the
Python Tutor and explain what is going on.
(We will explain the syntax soon... for now, simply try to guess what is going on)
(Wait; then Click)
- The class is "memorized" by Pyhton, but not used until we actualy instanctiate (stamp out) an object
- In line 7, we are using the class name to create an object (s1) of the type defined by our class (Student)
- The __init__ method takes care of initializing attributes for the class, which include:
- the students name, and
- the students list of grades
- Notice that we need to pass in, as parameters to the class's __init__ method, the values that we will copy into the class's attributes.
- in line 8.. we are printing the "reference" to the object s1
- in lines 9 and 10, we are printing the values we obtain by accessing the class's attributes using the dot operator.
The __init__ method returns the address of where the new object was created in memory (so it can be stored in an object reference, like s1 )
The __init__ method and the self
The
__init__ method is a specially named method inside the class that is activated when
first creating an object.
We'll talk more about this next class, but you can think of this as a class method that constructs the specific instance object from input parameters... sort of like a pizza being made to your exact specifications (never with pineapple).
The parameter
self, is a variable inside the object that automatically references itself. It allows you to say things like:
"make
my name, the value of the first parameter that was passed in"
or
self.name = name
One Class, Many Objects!
The power of this approach is that, while the Class "describes" the structure of each object,
it is each individual object that can be used and modified to hold different combinations of values (different state).
Try extending our example like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 | class Student:
def __init__(self, par_name, par_grades):
self.name = par_name
self.grades = par_grades
s1 = Student("Amy", [83, 88, 85, 94, 100])
s2 = Student("Beth", [73, 78, 75, 84, 90])
s3 = Student("Cathy", [63, 68, 65, 74, 80])
avg1 = sum(s1.grades)/len(s1.grades)
avg2 = sum(s2.grades)/len(s2.grades)
avg3 = sum(s3.grades)/len(s3.grades)
print ("{} got a {}\n".format(s1.name, avg1))
print ("{} got a {}\n".format(s2.name, avg2))
print ("{} got a {}\n".format(s3.name, avg3))
|
Activity 2 [2 minutes]:
Before running it, predict what is going on!
Now run it in the
Python Tutor
(Wait; then Click)
- In lines 7, 9, and 11, we instanctiate 3 different objects (from the same class!)
- In lines 13, 15, and 17, we use each setof internal values to calculate some useful numbers for each object
- We print some results
Instance Methods
In addition to the value attributes for each instance object, we can have each object be capable of perfoming actions inside its own functions, which we call methods.
For example, it is kind of annoying to say that we are encapsulating the values for each student, but have to calculate the grade averages "outside each object".
We can improve this by having the class also include a get_average method that does this for us.
Defining methods
Simply write the method inside the class and use the
self keyword to refer to "the instance whose method is being used right now".
Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 | class Student:
def __init__(self, par_name, par_grades):
self.name = par_name
self.grades = par_grades
self.average = -1
def get_average(self):
self.average = sum (self.grades)/len(self.grades)
return self.average
s1 = Student("Amy", [83, 88, 85, 94, 100])
s2 = Student("Beth", [73, 78, 75, 84, 90])
s3 = Student("Cathy", [63, 68, 65, 74, 80])
print ("{} got a {}\n".format(s1.name, s1.get_average()))
print ("{} got a {}\n".format(s2.name, s2.get_average()))
print ("{} got a {}\n".format(s3.name, s3.get_average()))
|
Note that the parameter for the method is
self but one
does not need to provide an argument for that parameter when invoking the method, since it is known for which instance it was called.
For example:
- in line 18, we call the method like this: s1.get_average(), so the object is s1
- in line 20, we call the method like this: s2.get_average(), so the object is s2
- in line 22, we call the method like this: s3.get_average(), so the object is s3
You can verify this in the
Python Tutor
Special methods
Special methods (those that should be "reserved" to do a special task) can be identified by the surrounding double underscores, like for __init__.
Class vs Object attributes
Continuing with our stencil analogy, you could:
Use the Stencil itself, to perform an action (make a sign)
\(\rightarrow\)
stamp out something using the stencil and use that instead (make a sign)
Another example:
Use the Stencil itself, to perform an action (draw or measure)
\(\rightarrow\)
stamp out something using the stencil and use that instead (use the stamped ruler to measure)
Using the Class itself: Attributes
A class can have
its own attributes!, which are
common to all instances for that class.
The following is an example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36 | class Student:
course_name = "CSC111"
num_students = 0
def __init__(self, par_name, par_grades):
self.name = par_name
self.grades = par_grades
self.average = -1
Student.num_students += 1
# this is also valid:
# self.num_students += 1
def get_average(self):
self.average = sum (self.grades)/len(self.grades)
return self.average
s1 = Student("Amy", [83, 88, 85, 94, 100])
s2 = Student("Beth", [73, 78, 75, 84, 90])
s3 = Student("Cathy", [63, 68, 65, 74, 80])
print ("{} got a {}\n".format(s1.name, s1.get_average()))
print ("{} got a {}\n".format(s2.name, s2.get_average()))
print ("{} got a {}\n".format(s3.name, s3.get_average()))
print (f"The course name is {Student.course_name} and it has {Student.num_students} students\n")
list_all = [s1.get_average(), s2.get_average(), s3.get_average()]
avg_all = sum(list_all)/len(list_all)
print ("The average for course {} is {}\n".format(Student.course_name, avg_all))
|
Try it out in the
Python Tutor
We will talk about
possibly building methods for the class in the future.
These would be able to modify ONLY class-owned attributes.
Object-Oriented Programming
So what does all of this (classes and objects) allow us to do?
It lets us program in a fundamentally different way.
The following image illustrates the difference:
Procedural Programming
This is what we've been doing so far:
- The program behaves like a recipe: one step at a time (in a general top-to-bottom pattern)
- We define entities that have properties and behaviors
- Entities interact with each other passing and extracting data through their respective interfaces (the methodsand attributes they expose to others)
-
Example programs look like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 | def flying_test(bird):
if bird == "Parrot":
print("Parrot can fly")
elif bird == "Penguin":
print("Penguin can't fly")
def swimming_test(bird):
if bird == "Parrot":
print("Parrot can't swim")
elif bird == "Penguin":
print("Penguin can swim")
def main():
blu = "Parrot"
peggy = "Penguin"
# passing the "variables"
flying_test(blu)
flying_test(peggy)
swimming_test(blu)
swimming_test(peggy)
if __name__ == "__main__":
main()
|
Object-Oriented Programming (OOP)
This is what we'll do from now on:
- The program simulates a real world set of entities interacting with each other!
- We can optimize the program by adding functions, or loops to complete repeated work
- Data is passed around and transformed in order to arrive at a desired outcome (like ingredients being processed from station to station)
Example programs look like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68 | # parent class
class Bird:
def __init__(self):
print("Bird is ready")
def whoisThis(self):
print("Bird")
# child class
class Penguin(Bird):
def __init__(self):
# call super() function
super().__init__()
print("Penguin is ready")
def whoisThis(self):
print("Penguin")
def run(self):
print("Run faster")
def fly(self):
print("Penguin can't fly")
def swim(self):
print("Penguin can swim")
# child class
class Parrot(Bird):
def __init__(self):
# call super() function
super().__init__()
print("Parrot is ready")
def whoisThis(self):
print("Parrot")
def run(self):
print("Run faster")
def fly(self):
print("Parrot can fly")
def swim(self):
print("Parrot can't swim")
# common interface
def flying_test(bird):
bird.fly()
def swimming_test(bird):
bird.swim()
#instantiate objects
blu = Parrot()
peggy = Penguin()
# passing the object
flying_test(blu)
flying_test(peggy)
swimming_test(blu)
swimming_test(peggy)
|
We will study this next class, in the meantime,
Check this out
Additional readings
Check out this article on Object-Oriented Programming
Also this one with a Course on using classes
Homework
[Due for everyone]
Homework 07 is Due Friday 04/01 by 5PM (With the usual extension until Monday 04/04 by 5PM)
[Optional]