Week 11, Monday: Classes



Objects

So far we have used lots of objects and their methods. Remember an object consists of both data and methods (functions). Here are some examples of objects we have used:

Way back in Week 2 we looked at a basketball stats example, where we had data for each basketball player stored in parallel lists: a list of player names, a list of points-per-game for each player, and a list of games played for each player:

players = ["Vladimir Radmanovic","Lou Williams","Lebron James", "Kevin Durant","Kobe Bryant","Kevin Garnett"]
ppg     = [0, 11.5, 30.3, 28.5, 30.0, 19.2]  # points-per-game
games   = [2, 13, 23, 20, 12, 20]            # games played

for i in range(len(players)):
  print("%d: %20s %4.1f %2d" % (i, players[i], ppg[i], games[i]))

This works but is klunky. And it would be worse if we tried to store more stats (rebounds, assists, blocked shots, etc) and more players (400+ for the NBA).

A better approach would be to create a custom object (an NBAPlayer object) and store all the data for that player in that object. Then we could just have one list of NBAPlayer objects in our program. We could also write custom methods to use on those objects -- anything we think we might need, such as a ppg() method to calculate a player's average points-per-game, or a playGame() method to record new data when a player plays another game.

Here's an example of how this class could be used:

>>> from basketball import *
>>> p1 = NBAPlayer("Jeff Knerr", 11, "Philadelphia 76ers")
>>> print(p1)
     Jeff Knerr #11 -- Team:   Philadelphia 76ers
                GP:   0,  PTS:   0
>>> print(p1.ppg())
0
>>> p1.playGame(35)
>>> print(p1)
     Jeff Knerr #11 -- Team:   Philadelphia 76ers
                GP:   1,  PTS:  35
>>> p1.playGame(30)
>>> print(p1)
     Jeff Knerr #11 -- Team:   Philadelphia 76ers
                GP:   2,  PTS:  65
>>> print(p1.ppg())
32.5

Notice how new objects are constructed: given a name, a jersey number, and a team name. When the player object is first constructed and printed, the stats are initially zero. And after the playGame() method is called twice (Jeff plays two good games!), the stats have changed.

Classes

To create the custom object above, we will need to define a class. Think of the class definition as a template: you want to create custom objects, and the class definition says how they are to be constucted, what data is stored in each object, and what methods can be applied to these objects. Once you have your class definition, you can create as many objects of that type as you want.

Here is some of the NBAPlayer class used above -- we will look at what it all means below.

class NBAPlayer(object):
  """class for single NBA player object"""

  def __init__(self, name, number, team):
    """constructor for player object, given name, etc"""
    # any self.whatever variable is DATA for this object,
    # and can be used in any methods in this class
    self.name = name
    self.number = int(number)    # jersey number
    self.team = team
    self.gp  = 0                 # games played
    self.pts = 0                 # points scored

  def __str__(self):
    """pretty-print info about this object"""
    s = "%15s #%i -- Team: %20s" % (self.name, self.number, self.team)
    s += "\n\t\tGP: %3d,  PTS: %3d" % (self.gp, self.pts)
    return s

  def playGame(self, points):
    """example of adding data to player object"""
    self.gp += 1
    self.pts += points

  def ppg(self):
    """calculate average points per game"""
    if self.gp == 0:
      return 0
    else:
      ave = self.pts/float(self.gp)
      return ave

Notes on the above class definition:

that self parameter

Notice that the constructor above has 4 parameters (self, name, number, team), but when it is called in the program (p1 = NBAPlayer("Jeff Knerr", 11, "Philadelphia 76ers")), only 3 arguments are used. The argument that corresponds to the self parameter is always implied. The self parameter simply refers back to which object we are talking about (e.g., p1).

If this is confusing to you, you are not alone! For now, just make sure all methods in the class definition have self as their first parameter.

Also notice, any self.whatever variable created in __init__ can be used in all other methods in the class, without being passed as parameters. For example, the playGame() method updates self.gp and self.pts.

adding a method

Suppose we want to add some additional functionality to our NBAPlayer class (maybe we are creating the software behind nba.com or espn.com/nba). Players are often traded from one team to another, so we would like to be able to do something like this:

>>> from basketball import *
>>> p1 = NBAPlayer("Jeff Knerr", 11, "Washington Bullets")
>>> p1.playGame(20)
>>> p1.playGame(10)
>>> p1.playGame(3)     # Jeff not playing well....let's trade him
>>> p1.trade("New York Knicks")
>>> print(p1)
     Jeff Knerr #11 -- Team:      New York Knicks
                GP:   3,  PTS:  33

Can you add the trade() method to the above class? As used above, it has one argument (the new team), so the method should have two parameters: self and some variable to hold the value of the new team (maybe newteam??). And the only thing this method does is change the value of the self.team instance variable.

Here's the new method:

def trade(self, newteam):
  """change team of player"""
  self.team = newteam

adding a new instance variable

What needs to change if we want to keep track of another statistic, such as number of rebounds? That requires another instance variable (self.rebounds) in the constructor, as well as modifying the playGame() method (add a rebounds parameter, and update self.rebounds). And like ppg(), we might want to make a whole new method (rpg()?) to calculate and return the average rebounds-per-game. You may also want to change the __str__ method to include the rebounding stats.

your turn...pizza class

Write a Pizza class that works with the following test code:

p1 = Pizza("cheese")
p2 = Pizza("mushroom and onion")
print(p1)
print(p2)

print("-"*20)
print("Num slices left in p2: %s" % p2.getSlices())
print("Eating a slice of %s!" % p2.getTopping())
p2.eatSlice()
print(p2)

print("-"*20)
for i in range(10):
  print("Eating a slice of %s!" % p1.getTopping())
  p1.eatSlice()
  print(p1)

And gives the following output:

cheese pizza :: slices left = 8
mushroom and onion pizza :: slices left = 8
--------------------
Num slices left in p2: 8
Eating a slice of mushroom and onion!
mushroom and onion pizza :: slices left = 7
--------------------
Eating a slice of cheese!
cheese pizza :: slices left = 7
Eating a slice of cheese!
cheese pizza :: slices left = 6
Eating a slice of cheese!
cheese pizza :: slices left = 5
Eating a slice of cheese!
cheese pizza :: slices left = 4
Eating a slice of cheese!
cheese pizza :: slices left = 3
Eating a slice of cheese!
cheese pizza :: slices left = 2
Eating a slice of cheese!
cheese pizza :: slices left = 1
Eating a slice of cheese!
cheese pizza :: slices left = 0
Eating a slice of cheese!
No slices left... :(
cheese pizza :: slices left = 0
Eating a slice of cheese!
No slices left... :(
cheese pizza :: slices left = 0