CS21 Week 13: Object-Oriented Programing
Week 13 Topics
-
Object-Oriented Programming
-
Defining Classes
-
The implicit self parameter
-
Class methods:
-
The constructor,
__init__
-
String conversion,
__str__
-
Access methods (getters)
-
Mutator methods (setters)
-
Monday
Finishing Recursion
Before moving on to new content, let’s wrap up recursion.
Objects Overview
So far, we have used lots of objects and their methods. Recall that an object consists of both data and methods — an object knows stuff and can do stuff. Here are some examples of objects we have used:
-
string objects (type
str
):-
data: characters in the string
-
methods:
.split()
,.strip()
,.isdigit()
, etc.
-
-
list objects (type
list
):-
data: elements in the list
-
methods:
.append()
,.sort()
, etc.
-
-
Zelle Graphics objects (e.g.
Circle
):-
data: radius, center point, fill color, etc.
-
methods:
.draw()
,.move()
,.setFill()
,.getCenter()
, etc.
-
-
HydroDam objects from labs 9 & 10
Why Objects?
Consider the student records CSV file we looked at during weeks 9 and 10; the first time through, we just stored this file as a list of strings (one per line), and then split each line into another list of strings (one per field).
Then later we used a class called StudentRecord to store each line instead of just a list of strings. This made our code easier to read, write, and understand; compare the following:
-
grade = students[i].get_GPA()
-
grade = float(lines[i].split(",")[3])
Even if both lines accomplish the same thing, it’s a lot more clear what’s going on in the first one. Our goal in using classes is to make development easier by increasing modularity and abstraction, so we don’t have to remember every detail (such as which index in a list of strings contains the GPA) at all times.
Classes and Object-Oriented Programming
In object-oriented programming, we saw that objects described
complex types that contained data and methods that could act upon the
data. For example, we saw that lists and strings were objects, as
were elements of the Graphics library. Each of the objects we created
stored data (e.g., a center, radius and color for Circle
objects)
and methods (e.g., move()
, getCenter()
, setFill()
).
Encapsulation
The idea of encapsulation is to take a bunch of related data and functionality, and package ("encapsulate") it together in a single object. This has several advantages:
-
can pass a single argument to a function, instead of having to pass a whole bunch of related parameters
-
once it’s written and works, we don’t need to worry about the details of how (e.g. we don’t know what the inside of the
list
class looks like, and that’s ok because we don’t need to)
Defining a Class
Creating a class defines a new type of object. Classes define the
specific methods and data an object stores. To start, let’s look at the
definition of the StudentRecord
class we’ve been using.
Open up student.py
. What do you think each line is doing? What can
you explain with your neighbor?
The first key element is the class definition, which follows the format:
class <ClassName>(object):
<ClassName>
is a placeholder for the actual name you want to give;
class
is a keyword to indicate that you are defining a new class.
The additional item, (object)
, is an indication that the class will
follow certain protocols (note that object
is what you’ll type, this one is
not a placeholder). You should assume that this will always be there (you
can put something else in there if you want to build upon an existing class,
but that’s beyond our CS 21 discussion).
Methods
Within the class, you will see several class methods. Methods follow the same format as functions, but have the distinction of belonging to a specific class. As always, Python uses indentation to define what belongs, so we every definition that’s part of a class must be indented in the class definition block.
class ClassName(object):
def __init__(self, param1, ...):
#Constructor - initializes object and data
def getter1(self):
#Return some piece of information
def setter1(self, valToSet):
#Change something about the data
General methods
Normal methods can be named and defined like any other function, but must have a
special parameter called self
, which must always be the first parameter.
self
is a reference to the object the method is being called on, and is used
in a method to refer to data in the object.
-
self.name
: a "name" field that’s part of the object referred to byself
; this is called an instance or member variable -
name
: this is just a regular variable that will behave like it would in any other function
The key distinction is that member variables are part of the object, so they last as long as the object does; this is different from normal "local" variables that go away when the function or method returns.
Special methods
Python has some special methods that use double underscores around their names; these get called automatically in some circumstances. There are a number of them, but the two most useful ones for us will be:
-
__init__
: the constructor, called automatically when an object is created -
__str__
: the to-string method, called automatically when an object is passed toprint()
Constructor
Python will run the __init__(self, x, y)
method of a class every time we
create an object of that type. Every class must have a constructor — it
specifies the initial data we need to keep track of and gives it a starting
value. There are many other special methods that we’ll skip for right now.
These include __len__
(to obtain the length of a sequence e.g., len(ls)
calls the list.__len__()
method) and __str__
(to convert an object to a
string representation, str(x)
).
Wednesday
self
parameter
self
is perhaps the single most confusing feature of python classes. You will
see self
everywhere when you are writing and designing a class. However,
you will not see self
at all when using a class, which is why we haven’t
talked about it until this week.
Notice that all methods in a class have a self
parameter as the first
parameter. This is required, but users of the class do not provide this
argument; instead Python automatically assigns the object that called the
method as the self
parameter. For example, if we create a StudentRecord
object, student
and call student.get_gpa()
, the self
parameter points to
student
. This is common source of confusion when writing your first class. If
you look at the definition of get_gpa(self)
, it looks like you need to pass
in one argument for the self
parameter. But since Python does this
automatically, users actually call this method with zero arguments, e.g.,
student.get_gpa()
. Calling get_gpa
with an extra argument will result in
a seemingly bizarre error message:
student.get_gpa(1) TypeError: get_gpa() takes exactly 1 argument (2 given)
Note that providing one user argument to get_gpa()
reports an error that
two arguments were provided. The automatic self
was the other (and
technically first) argument.
In summary, always put self
as the first parameter of a class method when
writing a method, but ignore this parameter when calling the method.
The meaning of self
You may be wondering: if self
is part of every method, but it’s ignored by
the user, why do we need it in the class definition? Recall that objects both
know stuff and do stuff. The method definitions inside the class are how
classes/objects do stuff, and the self
variable is how a particular object
knows stuff. In any method, we can declare a variable with a self.
prefix,
(e.g., self.course = "CS21"
). If we set the value of one of these special self.
attributes in one method, we can access the value in another method without
passing an additional parameter. In effect, all methods share all the self.
attributes.
Typically we use the __init__
method to define and initialize values for all
attributes we will be using at some point in the class.
Testing a Class
Just as with top-down design, you should practice incremental development when defining a class. Usually, this involves writing a function, thoroughly testing it, moving onto the next function, etc. There are several options for how to test, including using a separate main program. However, a common strategy is to include some test code at the bottom of your class definition file. This way, the test code can always be run in the future when updates are made. To do so, remember to include the following lines at the bottom of your file:
if __name__ == "__main__":
# Write testing code here
The first line ensures that the testing code does not run when you import the file, but rather only when someone calls the file from the command line:
$ python3 student.py
Exercises: Modify Student class and test
-
Let’s modify the
StudentRecord
class to add some getter and setter methods; -
be sure to test your program at each stage, and don’t move on until you’re confident it works correctly.
-
add a
get_age()
method to the class -
update the
print_student()
function inreaddata-obj.py
to use this new method -
add a
set_gpa()
method to the class -
add a
set_age()
method to the class; make sure it does not allow negative values
-
Friday
Objects, scope, and internal method calls
Reminder: inside a class definition, the object is referred to as self
. If we want to
access its data, we use dot notation e.g., self.name
is that records
the student’s name. The name
variable’s scope is the object
itself rather than the function it was originally defined in.
Similarly, from within a method of a object, you can call another method by
referring to the self
variable. For example, if a class has two methods,
method1(self, …)
and method2(self, …)
, you can call method1
from
within method2
:
class Thing(object):
def method1(self, x, y, z):
...
def method2(self, ...):
...
self.method1(a, b, c) # Call method1() on self from within method2
...
Why objects?
Objects provide encapsulation. In computer science, encapsulation can mean one of two related things:
-
A mechanism for restricting access to some of the object’s components.
-
A mechanism for bundling of data with methods operating on that data.
Classes and objects provide both mechanisms. On larger programming projects, it is common to have different people work on different parts of the program. Classes are a good place to divide the work. In this case, the class writer and the class user can agree on an interface. The interface specifies what methods the class has and what they should do. The class user doesn’t need to know or care how a class is implemented, only how to use the objects. The class writer doesn’t need to know or care how a class is used, only how to implement the interface.
Object-oriented programming also facilitates modularity. Modular programming is a software design technique that focuses on separating the functionality of a program into independent, interchangable pieces (aka "modules"), so that each piece contains everything needed to perform one aspect of the desired functionality.
Class definitions provide reusability, i.e., they let you create/reuse functionality while hiding technical details. With classes, you can rapidly create new and complex code by grabbing existing code "off the shelf" and reusing it for novel purposes.
Exercises: Modify Student class and test
-
Let’s modify the
StudentRecord
class to add a list of course grades; be sure to test your program at each stage, and don’t move on until you’re confident it works correctly.-
add a line to the constructor that creates an empty list called
self.grades
-
define a method
get_grades
that returns the list of grades -
define a method
add_grade
that takes a grade as a parameter and appends it to the list of grades -
define a method
compute_gpa
that goes through the list of grades and re-calculates the student’s GPA. This method should changeself.gpa
so it has the new value; it should also return that new value.
-