Week 12: Classes

Monday

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:

  • str objects: data = characters in the string; methods: lower(), split(), isalpha(), etc

  • list objects: data = item in the list; methods: sort(), append(), pop(), etc

  • Zelle Circle objects: data = radius, center point; methods: draw(), move(), setFill(), etc

example object: NBAPlayer

If we were storing statistics for NBA basketball players, we could use 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 nbaplayer import *
>>> p1 = NBAPlayer("Jeff Knerr", "phi")
>>> print(p1)
          Jeff Knerr -- Team: phi  GP:   0,  PTS:   0,  PPG:  0.0
>>> print(p1.ppg())
0
>>> p1.playGame(40)
>>> print(p1)
          Jeff Knerr -- Team: phi  GP:   1,  PTS:  40,  PPG: 40.0
>>> p1.playGame(20)
>>> print(p1)
          Jeff Knerr -- Team: phi  GP:   2,  PTS:  60,  PPG: 30.0
>>> print(p1.ppg())
30.0

Notice how new objects are constructed: given a name 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.

syntax of 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, team, gp=0, pts=0):
    """constructor for player object, given name, etc"""
    self.name = name
    self.team = team
    self.gp  = gp         # games played
    self.pts = pts        # total points scored

  def __str__(self):
    """
    create string with info about this object.
    if you print one of these objects, this is how it will look.
    """
    s = "%20s -- Team: %3s" % (self.name, self.team)
    s += "  GP: %3d,  PTS: %3d,  PPG: %4.1f" % (self.gp, self.pts, self.ppg())
    return s

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

  def ppg(self):
    """calculate player's 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:

  • the __init__ method is called the constructor

  • the constructor can have 1 or more parameters (the above has 5, but one is special)

  • the first parameter in all of the methods is self, and this refers back to whatever object we are applying the method to (more about this below)

  • in a program that uses this class, the constructor is called by using the name of the class (e.g., p1 = NBAPlayer("Jeff Knerr", "phi"))

  • all self.whatever variables (called instance variables) in the constructor are the data stored in each object

  • the __str__ method is used whenever an object is printed (e.g., print(p1))

  • the __str__ method should return a string (not print a string)

  • when you design a new object, you can write the constructor to have as many parameters as you want, have as many instance variables as you need, have the object print however you want (using the __str__ method), and have as many other methods as you see fit!

  • if this were in a file called nbaplayer.py, we could use it in other programs by adding from nbaplayer import * to the other programs

that self parameter

Notice that the constructor above has 5 parameters (self, name, team, gp=0, pts=0), but when it is called in the program (p1 = NBAPlayer("Jeff Knerr", "phi")), only 2 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).

Also, the last two parameters (gp=0, pts=0) are a way to include default arguments. When constructing objects, if they are specified, they will have those values (ex: p1 = NBAPlayer("Jeff","phi",10,200)), and if not specified, they will have the default values (in this case, both zero).

If self 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 nbaplayer import *
>>> p1 = NBAPlayer("Jeff Knerr", "phi")
>>> p1.playGame(20)
>>> p1.playGame(10)
>>> p1.playGame(3)     # Jeff not playing well....let's trade him
>>> p1.trade("bos")
>>> print(p1)
      Jeff Knerr -- Team: bos  GP:   3,  PTS:  33,  PPG: 11.0

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.

write the Cake(..) class

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

>>> from cake import *
>>> c1 = Cake("Chocolate")
>>> c1.slice(8)
>>> print(c1)
Chocolate cake (slices left: 8).
>>> c2 = Cake("Carrot")
>>> c2.slice(12)
>>> print(c2)
Carrot cake (slices left: 12).
>>> c1.serve()
Here's your slice of Chocolate cake!
>>> print(c1)
Chocolate cake (slices left: 7).

And make sure your cake class accounts for running out of slices!

>>> for i in range(10):
...    c1.serve()
...
Here's your slice of Chocolate cake!
Here's your slice of Chocolate cake!
Here's your slice of Chocolate cake!
Here's your slice of Chocolate cake!
Here's your slice of Chocolate cake!
Here's your slice of Chocolate cake!
Here's your slice of Chocolate cake!
No Chocolate cake left! :(
No Chocolate cake left! :(
No Chocolate cake left! :(

Wednesday

finish the cake class

What if we want to prevent slicing the cake twice? Can you check to see if the cake was already sliced??

>>> from cake import *
>>> c = Cake("Carrot")
>>> c.slice(8)
>>> c.slice(10)
You already sliced this cake!

One way to do this is to add a new instance variable, to keep track of if the cake has already been sliced.

Here’s an example of the full Cake class:

class Cake(object):
    """cake class"""

    def __init__(self, flavor):
        """constructor for cake objects, given flavor"""
        self.flavor = flavor
        self.slices = 0
        self.sliced = False   # initially, cakes have not been sliced

    def __str__(self):
        """must RETURN A STRING!!!"""
        if self.sliced:
          s = "%s cake (slices left: %d)." % (self.flavor,self.slices)
        else:
          s = "%s cake (unsliced)." % (self.flavor)
        return s

    def slice(self, nslices):
        """cut the cake into nslices"""
        if self.sliced:
          print("You already sliced this cake!")
        elif nslices<1 or nslices>50:
          print("That's a crazy number of slices!")
        else:
          self.slices = nslices
          self.sliced = True

    def serve(self):
        """serve out one slice of cake"""
        if self.slices > 0:
            print("Here's your slice of %s cake!" % self.flavor)
            self.slices -= 1
        else:
            print("No %s cake left! :( " % self.flavor)

    def getFlavor(self):
        """getter for flavor data"""
        return self.flavor

    def getSlices(self):
        """getter for slice data"""
        return self.slices

zturtle class

Turtle graphics is another way to draw things, but instead of graphics objects (like Circles and Rectangles), we control a pen (or a turtle), and tell it to move forward or change it’s heading. It’s called turtle graphics because, like a turtle walking across the sand, it leaves a trail.

Here’s and pseudo-code example of drawing a square using turtle graphics:

create a turtle object
for i in range(4):
   move forward (and draw) a certain amount
   turn by 90 degrees

Let’s create a turtle object using the Zelle Graphics Library.

If you run update21, you should get a zturtle.py file. That file includes a ZTurtle class with a contructor and the forward(..) function. Your job is to finish the Zturtle class — write any of the methods below that aren’t already done (see the zturtle.py file). Also, see testturtle.py for an example of how the ZTurtle object is used. And start by writing the methods that are needed in that file (turn(degrees) and setColor(color)).

Here’s the full documentation for the ZTurtle class:

class ZTurtle(object)
 |  Zelle Graphics Turtle
 |
 |  __init__(self, x, y, window)
 |      construct new turtle object with given x,y position
 |
 |  __str__(self)
 |      return a string with turtle info (x,y,heading,tail status)
 |
 |  down(self)
 |      lower the tail
 |
 |  forward(self, ds)
 |      move forward a distance ds, draw if tail is down
 |
 |  getHeading(self)
 |      return the current heading
 |
 |  getX(self)
 |      return the current x coordinate
 |
 |  getY(self)
 |      return the current y coordinate
 |
 |  moveto(self, x, y)
 |      move turtle, without drawing, to given x,y location
 |
 |  setColor(self, color)
 |      set the drawing color
 |
 |  setHeading(self, h)
 |      set heading to given heading h
 |
 |  turn(self, amount)
 |      change the heading by the given amount (degrees)
 |
 |  up(self)
 |      raise the tail

Once you have the zturtle class working, edit/try a few of these programs (they each have starter code in them, you just need to finish them):

testturtle.py

testturtle

spiral.py

spiral

boxspiral.py

boxspiral

star.py

star

walk.py

walk

tree.py

tree

koch.py

koch

Friday

No class — Thanksgiving Break!

Swatify Music App

Suppose we are writing a music app that has songs and playlists. We want the user to be able to make and edit playlists of songs, and, obviously, play the songs on the playlist.

Here’s a simplified example of how the objects could be used (see swatify.py):

from song import *
from playlist import *

def main():
  p1 = Playlist("Jeff's Awesome Playlist")
  # show initial, empty playlist
  print(p1)

  # read in song data, add some songs to playlist
  songs = readFile("songs.csv")
  p1.add(songs[0])
  p1.add(songs[2])
  p1.add(songs[4])
  print(p1)

  # toggle the shuffle, play the playlist
  p1.toggleShuffle()
  p1.play()

def readFile(fn):
  """read in song data from file, one song per line"""
  #title, running time, artist, date-of-purchase, number-of-plays
  #Badlands,4:04,Bruce Springsteen,9/14/05; 10:00 PM,60
  songs = []
  inf = open(fn,"r")
  for line in inf:
    data = line.strip().split(",")
    title = data[0]
    rtime = data[1]
    artist = data[2]
    dop = data[3]
    nplays = data[4]
    # all song objects have a song title, artist, running time, and number of plays
    s = Song(title,artist,rtime,int(nplays))
    songs.append(s)
  inf.close()
  return songs

main()

And here is the output of the above code (just an example of how it might look):

$ python3 swatify.py
Playlist: Jeff's Awesome Playlist -- total running time: 0hr:0min:0sec
[shuffle=False, public=True]
No songs in this playlist...

Playlist: Jeff's Awesome Playlist -- total running time: 0hr:10min:25sec
[shuffle=False, public=True]
 1: The A Team by Ed Sheeran
 2: Almost Like Being In Love by Frank Sinatra
 3: Badlands by Bruce Springsteen

playing Badlands...by Bruce Springsteen
playing The A Team...by Ed Sheeran
playing Almost Like Being In Love...by Frank Sinatra

We’ll start by making the Song and Playlist classes.

the Song(..) class

Each Song object should have these instance variables:

  • title: title of the song

  • artist: song artist

  • running time: length of the song — a string, like '3:05'

  • number of plays: how many times the song has been played (an integer)

Here’s the full documentation for the Song class:

class Song(builtins.object)
 |  Song object class
 |
 |  Methods defined here:
 |
 |  __init__(self, title, artist, rtime, nplays)
 |      constructor, given title, artist, running time, number of plays
 |
 |  __str__(self)
 |      should return a string with song info
 |
 |  getArtist(self)
 |      getter for song artist
 |
 |  getPlays(self)
 |      getter for number of times song has been played
 |
 |  getRTime(self)
 |      getter for running time of song (string, like '4:02')
 |
 |  getSeconds(self)
 |      getter for running time of song in seconds (int, like 242)
 |
 |  getTitle(self)
 |      getter for title of song
 |
 |  play(self)
 |      dummy 'play' method, just says playing song and calls sleep(1)
 |      (also increments number of plays)

If you look in the songs.csv file, you’ll see each song in there has a title, a running time (a string, like '5:37'), an artist, a date-of-purchase (which we are not using), and a number (integer) for how many times the song has been played.

Your job is to add the above class and methods to song.py, so we can create Song objects from the (fake) data in songs.csv.

Here is some test code you could use to make sure your Song class is working:

  s = Song("A","B","4:30",10)
  print(s)
  s.play()
  print(s)
  print(s.getTitle())
  print(s.getArtist())
  print(s.getRTime())
  print(s.getPlays())
  print(s.getSeconds())

And running it would show this:

$ python3 song.py
A by B (4:30, 10)
playing A...by B
A by B (4:30, 11)
A
B
4:30
11
270

The only tricky method is getSeconds() which returns the running time as an integer number of seconds. In the above example, the running time is '4:30', which getSeconds() uses to calculate and return the total number of seconds (270).

the Playlist(..) class

Once you have the Song class working, write the playlist class.

The user should be able to create a new playlist, given the title of the playlist. They should also be able to add songs to the playlist. All Playlist objects should have the following instance variables:

  • title: title of the playlist

  • songs: list of songs in the playlist (initially empty)

  • time: running time of full playlist (all songs) in seconds (initially 0)

  • public: boolean for playlist status (defaults to True (public playlist))

  • shuffle: boolean for playing the songs (initially False, so play in order)

Here’s the documentation for the Playlist objects:

class Playlist(builtins.object)
 |  Playlist class
 |
 |  Methods defined here:
 |
 |  __init__(self, title)
 |      constructor for Playlist objects, given title of playlist
 |
 |  __str__(self)
 |      should return a string with playlist info
 |
 |  add(self, song)
 |      add given song to the playlist
 |
 |  play(self)
 |      play the playlist (call each song's play method)
 |
 |  toggleShuffle(self)
 |      toggle the shuffle instance variable (boolean)

And here’s some test code to check your playlist objects:

  p1 = Playlist("Best Playlist Ever")
  print(p1)
  s = Song("A","B","4:30",10)
  p1.add(s)
  p1.add(s)
  print(p1)

and the output (depending on how you wrote your str method):

Playlist: Best Playlist Ever -- total running time: 0hr:0min:0sec
[shuffle=False, public=True]
No songs in this playlist...

Playlist: Best Playlist Ever -- total running time: 0hr:9min:0sec
[shuffle=False, public=True]
 1: A by B
 2: A by B

the swatify app

Once you have your song and playlist objects working, try the swatify.py app to make sure it works.

if you have time

Add a menu() function to the swatify app to give the user 4 options: add, show, play, quit. Here’s one example of how this could work:

$ python3 swatify.py
add|show|play|quit
What do you want to do? show
Playlist: Jeff's Awesome Playlist -- total running time: 0hr:0min:0sec
[shuffle=False, public=True]
No songs in this playlist...

add|show|play|quit
What do you want to do? add
 1. The A Team by Ed Sheeran
 2. A.M. Radio by Everclear
 3. Almost Like Being In Love by Frank Sinatra
 4. Animal Song by Loudon Wainwright III
 5. Badlands by Bruce Springsteen
 6. Breakdown by Tom Petty & The Heartbreakers
 7. Do You Feel by The Rocket Summer
 8. Everyday Superhero by Smash Mouth
 9. Gonna Fly Now by Bill Conti
10. It's On by Superchick
11. Like a Rolling Stone by Bob Dylan
12. Message In A Bottle by The Police
13. Since U Been Gone by Kelly Clarkson
14. Sweet Caroline by Glee Cast
15. Take It Easy by Eagles
16. This Is The Life by Hannah Montana
17. Welcome To The Machine by Pink Floyd
18. You Get What You Give by New Radicals
Which song do you want to add? 5
add|show|play|quit
What do you want to do? add
 1. The A Team by Ed Sheeran
 2. A.M. Radio by Everclear
 3. Almost Like Being In Love by Frank Sinatra
 4. Animal Song by Loudon Wainwright III
 5. Badlands by Bruce Springsteen
 6. Breakdown by Tom Petty & The Heartbreakers
 7. Do You Feel by The Rocket Summer
 8. Everyday Superhero by Smash Mouth
 9. Gonna Fly Now by Bill Conti
10. It's On by Superchick
11. Like a Rolling Stone by Bob Dylan
12. Message In A Bottle by The Police
13. Since U Been Gone by Kelly Clarkson
14. Sweet Caroline by Glee Cast
15. Take It Easy by Eagles
16. This Is The Life by Hannah Montana
17. Welcome To The Machine by Pink Floyd
18. You Get What You Give by New Radicals
Which song do you want to add? 10
add|show|play|quit
What do you want to do? show
Playlist: Jeff's Awesome Playlist -- total running time: 0hr:7min:32sec
[shuffle=False, public=True]
 1: Badlands by Bruce Springsteen
 2: It's On by Superchick

add|show|play|quit
What do you want to do? play
playing Badlands...by Bruce Springsteen
playing It's On...by Superchick
add|show|play|quit
What do you want to do? quit
$

if you have lots of time

What other features would you add to this app and the classes? Here are some ideas:

  • delete(song) method for the playlist class (allow user to delete a song from a playlist)

  • edit(playlist) function for the swatify app (allow user to select songs to move around or delete, or add more songs)

  • toggle method for the self.public instance variable in the playlist class

  • add a search(string) function to swatify app to allow user to search for songs (e.g., show all songs by a given artist), then have the option of adding them to a playlist

make it really play sound

Let’s add a real play() method to the Song class!

I put a new songs.csv file and some mp3 files in /scratch/knerr/swatify. You can see these with this command: ls /scratch/knerr/swatify

And you can see the songs.csv file with this (in a terminal):

cat /scratch/knerr/swatify/songs.csv

If you do that, you’ll notice an additional item on each line: an mp3 file. We need to modify the jk_song.py file to add this additional item:

  1. add an extra parameter to __init__:

     def __init__(self,title,artist,rtime,nplays,audiofile):
  2. and add an extra instance variable to init:

     self.audiofile = audiofile
  3. add "import subprocess" at the top of the file (so we can call unix commands from python)

  4. change the play() method to do this instead of sleeping:

     subprocess.call("mplayer %s" % self.audiofile, shell=True)

The subprocess.call() line just runs the mplayer command on the given audiofile (just like we did on the command line).

Now try running jk_swatify.py to see if it works

Also note, if you try this on other lab computers, sound may or may not work. Here’s a help page we have if you are trying to get sound to work on the CS lab machines (see the "Room 256" section): audio help page