Week 12: Classes
Announcements
-
Lab 10 is available now.
Week 12 Topics
-
Object-Oriented Programming
-
Defining Classes
-
The implicit self parameter
-
Class methods
-
The constructor, init
-
String conversion, str
-
Access methods (getters)
-
Mutator methods (setters)
Overview
So far, we have used lots of objects and their methods. Remember 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:
-
str objects: data = characters in the string. Methods:
split()
,lower()
, etc. -
list objets: data = items in the list. Methods:
append()
,pop()
, etc. -
Zelle Circle objects: data = radius, center point, fill color, etc. Methods:
draw()
,move()
,setFill()
,getCenter()
, etc. -
Donation objects
-
Candidate objects
Today, consider the task of managing some pizza orders. Each pizza you make has a number of slices and a topping (unless it is plain). The pizza can be cooked or not . Things we might want to do with a pizza include adding a topping, making the pizza, and serving a slice (as long as the pizza has already been made)
Writing a program would first need to establish the data, e.g.:
pizza1 = ["plain", 8, True] # True represents that it has been cooked
pizza2 = ["pepperoni", 6, True]
pizza3 = ["mushroom", 8, False]
pizzas = [pizza1, pizza2, pizza3]
Now, let’s say we want to order a slice:
pizza = input("Which pizza do you want? ")
for i in range(len(pizzas)):
if pizzas[i][0] == pizza:
if(pizzas[i][2] and (pizzas[i][1] > 0)):
pizzas[i][1] -= 1
print("Here is your slice of %s pizza. There are %d slices left."
% (pizza, pizzas[i][1]))
We could extend this to add data and other functionality. However, this isn’t a very satisfying solution. The code will quickly get unwieldy, becoming susceptible to bugs. It also isn’t easy to read, or reuse for a different application. We will return to the concept of object-oriented programming to come up with a better solution.
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()
).
Classes are the types of an object; they define the specific methods and data an object stores. We will spend the next two weeks defining classes and creating objects from them. To start, we’ll consider a class definition to solve the problem above.
Open up pizza.py
. Read through the mostly completed class
definition for a pizza. Using this class, we will be able to define
Pizza
objects.
Defining a Class
Open up pizza.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
have particular default operations and methods. 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 is beyond our 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.
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
Two broad categories of methods are accessors (or getters) and mutators (or setters). Some methods do both. Methods are the primary method for interfacing with the data maintained within the class - we do not directly touch the data, but rather use methods to get/set information.
For example, consider lists. When we want to add a value to the end
of the list, we do not directly access the data within the list.
Rather, we use the append(x)
method which handles that work for us.
Python also has special built-in methods that are flanked by
underlines e.g., init()
. init
gets called whenever we
call the constructor to create a new object. For example, when
creating a Point
object:
pt = Point(x,y)
Constructor
Python will run the init(self,x,y)
method of the Point
class.
Every class must have a constructor - it specifies what data we need
to keep track of and gives it an initial value. There are other
special methods that we will not cover. 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)
).
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 Pizza
object,
pizza
and call pizza.serveSlice()
, the self
parameter points to
pizza
. This is common source of confusion when writing your first class. If you look at the definition of serveSlice(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., pizza.serveSlice()
. Calling serveSlice
with an extra argument will result in a seemingly bizarre error message:
yum.serveSlice(1) TypeError: serveSlice() takes exactly 1 argument (2 given)
Note that providing one user argument to serveSlice()
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.
So if self
is part of every method, but is 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.cooked=False
. 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.
Pizza class
Test out how the Pizza
class works. What is the output of each of
these commands, and can you draw the stack diagram that accompanies
it?
>>> from pizza import * >>> pizza = Pizza("mushroom") >>> print(pizza) >>> pizza.makePizza() >>> pizza.serveSlice() >>>
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__`:
#Include 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 pizza.py
Exercises: Implement Pizza class and test
-
Take some time to implement and test the
addTopping()
method. For now, only allow one topping per pizza. Also, make sure the pizza hasn’t yet been cooked. -
Analyze and test the
serveSlice()
function. -
Implement and test a
removeTopping()
function, which removes the topping if the pizza hasn’t yet been made. -
If you have time, modify the Pizza class to allow for multiple toppings. How does this change the data in the Pizza class? How would this change the methods?
Objects, scope, and internal method calls
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.balance
is
the balance on the account. This means that the variable’s scope is
the object itself, not the function that it was created.
It should be noted that the definition of an object includes data
and methods. So, if we want to refer to a call a method we need to
use dot notation since the method belongs to the object e.g.,
pizza.serveSlice()
. But what if we want to call a method within
another method? That is, within pizza.py
, we wanted to call a
method? Well, since self
is the object and the method we want to
call belongs to self
, we can call it e.g., self.serveSlice()
As an example, take a look at the serveSlice()
metehod in pizza.py
def makePizza(self):
self.cooked = True
print("pizza is ready!")
Tips for writing your own class
-
Always put
self
as the first parameter of every method when defining a new method. -
Since python automatically adds the object as
self
as the first argument when calling any method, you do not need to explicitly pass aself
argument when calling a method. -
A
self.varname
defines a class attribute. An attribute defined in one method can be used later in any other method within the same class. -
The
init
method should give an initial value for all attributes of a class. -
Write an
str
to return a string representation of your object. This method is automatically called when you print an object, or convert an object to a string usingstr()
. -
Do not access attributes directly when using the class. Instead, use methods to access or modify the attributes.
-
If a method needs to call another method within the same class on the same object, use
self.method_name(…)
to call the other method.
Exercise: Team class
Implement a class called Team that stores and manipulates information about a competitive team (e.g., soccer, debate, baseball). Each team keeps track of its name, year, city, wins, and losses. In addition:
-
The constructor should take parameters for the name, year, and city. It should initializes the number of wins and losses to 0.
-
Write a method to create a string summarizing the team’s data (e.g., "2018 Wolverines: 12 wins, 0 losses")
-
Include a getter for each data member
-
Have a method
wonGame
that increments the team’s number of wins by 1. -
Similarly, have a
lostGame
that updates the number of losses. -
Write a method,
getWinPercent
that returns the percentage of games won out of the total number of games played. If a team has played 0 games, return a winning percentage of 0.0. If a team has won 3 out of 4 games, return a winning percentage of 0.75