Week 12: classes and objects

Monday

The video from today’s lecture (Monday, April 20, 2020):

And here are the annoucements:

  • Lab 10 is up and due Saturday (Apr 25)

  • Quiz 5 is out and due Friday

This week and next are all about Object-Oriented Programming (OOP) and creating our own objects!

the class file

Look at restaurant.py for example of a class file. This file defines the Restaurant object data and methods. Also see rapp.py for example of using the Restaurant class.

class Restaurant(object):
    """restaurant class"""

    def __init__(self,name,city,state,stars,reviewcount):
        """constructor for Restaurant objects"""

        # these are the instance variables
        self.name = name
        self.city = city
        self.state = state
        self.stars = stars
        self.reviewcount = reviewcount
        # every restaurant object contains these data

    def getName(self):
        """getter for name of restaurant"""
        return self.name

    def getCity(self):
        """getter for city of restaurant"""
        return self.city

    def getState(self):
        """getter for state of restaurant"""
        return self.state

    def getStars(self):
        """getter for stars of restaurant"""
        return self.stars

    def getReviewCount(self):
        """getter for review counts of restaurant"""
        return self.reviewcount

# NOTE: for **all** methods, self is the first parameter.
# self refers back to *which* object we are operating on

    def setStars(self,s):
        """setter for stars of restaurant"""
        self.stars = s

    def setReviewCount(self,rc):
        """setter for review counts of restaurant"""
        self.reviewcount = rc

Lots of new syntax and terminology to learn!

  • class definition

  • constructor (the init method)

  • instance variables (the self.whatever variables in init)

  • the "self" parameter (refers to the object)

  • getters and setters

  • class WRITER vs class USER: in this example, the person writing the rapp.py program is the class USER

playing card games

If you were going to write a card game program like solitaire or poker, what objects would you include in your application (game)?

Two possible objects are a Card object to represent each card in the game, and a Deck object to represent a deck of playing cards.

I’ve already written a version of the Card class. Let’s look at that and then write the Deck class together.

Here is the Card class in card.py:

class Card(object):
    """card object for playing cards"""

    def __init__(self, rank, suit):
        """create playing card, given rank and suit"""
        self.rank = rank   # A23456789TJQK
        self.suit = suit   # CDHS

    def __str__(self):
        """return string representation of playing card"""
        return self.rank+self.suit

    def getRank(self):
        """getter for card rank"""
        return self.rank

    def getSuit(self):
        """getter for card suit"""
        return self.suit

def main():
    """test code here"""
    cards = []
    for rank in "A23456789TJQK":
        c = Card(rank,"H")   # all the hearts
        cards.append(c)
    for c in cards:
        print(c)

if __name__ == "__main__":
    main()

In the above, each card object has rank (ace,two,three..) and suit (hearts,clubs…​). The test code at the bottom of the file shows an example of creating a card object (e.g., c = Card("4","H")). Besides the constructor, the only other methods are getters for the rank and suit, and the str method, which gets automatically called when we try to print card objects.

Now that we have Card objects, let’s make a deck of cards!

In deck.py create the Deck class. Each deck of cards should contain 52 playing cards (i.e., a list of Card objects).

Here’s one way to write the constructor for the Deck class:

    def __init__(self):
        """constructor for deck of cards"""
        self.cards = []
        for suit in "CDHS":  # clubs, diamonds,...
            for rank in "A23456789TJQK":
                c = Card(rank,suit)
                self.cards.append(c)

Can you add these other methods to the deck class?

__str__       returns string representation of Deck object
shuffle()     shuffles the cards in the deck
dealCard()    removes a card from the deck and returns it

Wednesday

The video from today’s lecture (Wed, April 22, 2020):

more Deck class

Let’s add the shuffle and dealCard methods to the Deck class, and then test everything.

    def shuffle(self):
        """shuffle the deck of cards"""
        for i in range(len(self.cards)):
            # pick a random index (ri)
            ri = randrange(len(self.cards))
            self.cards[i],self.cards[ri] = self.cards[ri],self.cards[i]

    def dealCard(self):
        """deal/return one card from the deck"""
        card = self.cards.pop()
        return card

So now if you create a deck and deal out some cards, you should see that many fewer cards in the deck when you print the deck:

def main():
    """test code for deck class"""
    d = Deck()
    print(d)
    d.shuffle()
    print(d)                 # cards should be shuffled
    for i in range(5):
        c = d.dealCard()
        print(c)
    print(d)                 # deck should have 5 less cards in it

writing a card game

I made a simple bridge.py file that uses the Card and Deck classes (as well as a Hand class). My goal was to show how all of the classes are imported and used, if we were going to write a real game to play contract bridge. I didn’t write the full game, but you should be able to see how I shuffle the deck of cards and then deal out 4 hands.

the Cake class

Your turn…​make a Cake class! I want to be able to use the cake objects like this:

    c1 = Cake("chocolate")     # create a chocolate cake
    c2 = Cake("carrot")        # create a carrot cake
    print(c1)                  # says it's a chocolate cake, unsliced
    print(c2)                  # says it's a carrot cake, unsliced
    c1.slice(8)                # cut the chocolate cake into 8 slices
    c2.slice(16)               # cut the carrot cake into 16 slices
    print(c1)                  # says it's a chocolate cake with 8 slices left
    print(c2)                  # says it's a carrot cake with 16 slices left
    for i in range(10):
        c1.serve()             # serves one of the chocolate cake slices
        print(c1)

What data and methods do all Cake objects need? Here’s the output I am looking for, from running the above code. See if you can write the cake.py file!

chocolate cake has not been sliced yet
carrot cake has not been sliced yet
chocolate cake -- slices left: 8
carrot cake -- slices left: 16
Here's your slice of chocolate cake!
chocolate cake -- slices left: 7
Here's your slice of chocolate cake!
chocolate cake -- slices left: 6
Here's your slice of chocolate cake!
chocolate cake -- slices left: 5
Here's your slice of chocolate cake!
chocolate cake -- slices left: 4
Here's your slice of chocolate cake!
chocolate cake -- slices left: 3
Here's your slice of chocolate cake!
chocolate cake -- slices left: 2
Here's your slice of chocolate cake!
chocolate cake -- slices left: 1
Here's your slice of chocolate cake!
chocolate cake -- slices left: 0
Sorry, no cake left! :(
chocolate cake -- slices left: 0
Sorry, no cake left! :(
chocolate cake -- slices left: 0

Friday

The video from today’s lecture (Friday, April 24, 2020):

And here are the annoucements:

  • Lab 10 is up and due Saturday (Apr 25)

  • Quiz 5 is out and due Friday

  • Lab 11 will be posted on Sunday

  • Final exam is now just Quiz 6, on recursion and classes/objects

more cake

Last time we were writing the Cake class. Here’s what I wrote:

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

    def __init__(self, flavor):
        """create cake of given flavor"""
        self.flavor = flavor
        self.sliced = False # boolean to tell if cake has been sliced
        self.slices = None  # cake hasn't been sliced yet
                            # hmmm...now that I look at that, I could
                            # probably just use self.slices==None to determine
                            # if the cake has been sliced or not...oh well.

    def __str__(self):
        """should return a string"""
        s = "%s cake" % (self.flavor)
        if self.sliced:
            s += " -- slices left: %d" % (self.slices)
        else:
            s += " has not been sliced yet"
        return s

    def slice(self, nslices):
        """cut cake into n slices"""
        if not self.sliced:
            self.slices = nslices
            self.sliced = True

    def serve(self):
        """serve out a slice of the cake"""
        if self.sliced:
            if self.slices > 0:
                print("Here's your slice of %s cake!" % (self.flavor))
                self.slices = self.slices - 1
            else:
                print("Sorry, no cake left! :( ")
        else:
            print("Cake hasn't been sliced yet!")

def main():
    c1 = Cake("chocolate")     # create a chocolate cake
    c2 = Cake("carrot")        # create a carrot cake
    print(c1)                  # says it's a chocolate cake, unsliced
    print(c2)                  # says it's a carrot cake, unsliced
    c1.slice(8)                # cut the chocolate cake into 8 slices
    c2.slice(16)               # cut the carrot cake into 16 slices
    print(c1)                  # says it's a chocolate cake with 8 slices left
    print(c2)                  # says it's a carrot cake with 16 slices left
    for i in range(10):
        c1.serve()             # serves one of the chocolate cake slices
        print(c1)

if __name__ == "__main__":
    main()

adding assert() statements

The above works, and I can look at the output from main() and check that everything is the way it should be. It would be nice to have the computer automatically check stuff (make the computer do the work!), so I don’t have to scan all of the outputs from print() statements. The assert(..) statement is one way to automate the testing!

Suppose I want to test that my serve() method is working correctly, and doesn’t serve out more slices of cake than we have. If I add this getter for the number of slices:

    def getSlices(self):
        """getter for number of slices left"""
        return self.slices

Then I can use that and test that it’s giving the numbers I think it should.

Here’s some test code using assert statements:

    flavor = "AAAA"           # make a fake Cake object
    n = 10
    fake = Cake(flavor)

    assert(fake.getSlices()==None)   # this should be True
    fake.slice(n)
    assert(fake.getSlices()==10)     # and so should this

Remember, for assert(…​), you will see no output if what is inside the parens evaluates to True. By writing those asserts, I am saying, if my code is working properly, those statements should be True. Now when I run the test code, no output means all my assert tests passed. If I get an AssertionError, then something must be wrong.

adding the len() function

For some objects, like strings and lists, using the len() function makes sense. For strings it tells us the length of the string, and for lists it tells us how many items are in the list. For a Restaurant object, length doesn’t really mean anything useful, so we wouldn’t want to use the length function on a Restaurant object. What about for cakes? Let’s say the length of a Cake is "how many slices are left". Can we add something to the Cake class so we can use len() on our cake objects?

Here’s what happens when we try to use it now:

TypeError: object of type 'Cake' has no len()

If, however, we add this method to the Cake class:

    def __len__(self):
        """make len() function work for our cakes"""
        return self.slices

Now look what happens:

>>> from cake import *
>>> yum = Cake("Birthday")
>>> yum.slice(12)
>>> print(len(yum))
12
>>> yum.serve()
Here's your slice of Birthday cake!
>>> print(len(yum))
11

the Contact class

Suppose we want to write a Contact class to help keep track of our contacts. Each contact could have a name (first and last), email, and phone number.

Here’s a simple contact.py file:

class Contact(object):
    """Contact Class"""

    def __init__(self):
        """contstuctor for empty contact objects"""
        self.last = ""    # last name
        self.first = ""   # first name
        self.email = ""
        self.phone = ""

    def __str__(self):
        """should return a string..."""
        s = "%s, %s -- %s, %s" % (self.last, self.first,
                self.email, self.phone)
        return s

    def setLast(self, lastname):
        """setter for last name"""
        self.last = lastname
    def setFirst(self, firstname):
        """setter for first name"""
        self.first = firstname
    def setEmail(self, email):
        """setter for email"""
        self.email = email
    def setPhone(self, phone):
        """setter for phone"""
        self.phone = phone

For the above, we are assuming contacts will first be created empty, and then all information will be added using the setters:

    c = Contact()
    c.setLast("Knerr")
    c.setFirst("Jeff")
    c.setEmail("jknerr1@swat.edu")
    c.setPhone("614-690-5758")
    print(c)

Maybe we want to make it a little easier to add names, providing the full name as "First Last". Let’s write another method to handle that:

    def setName(self, fullname):
        """set first and last, given full name"""
        first,last = fullname.split()
        self.first = first
        self.last = last

Now something like this should work:

    c2 = Contact()
    c2.setName("George Washington")
    print(c2)

Finally, what if we want a way to create that contact, providing the full name to the constructor? Python allows you to specify parameters and a default value, like this:

    def __init__(self,fullname=""):
        """contstuctor for contact objects, allows given fullname"""
        if fullname == "":
            self.last = ""    # last name
            self.first = ""   # first name
        else:
            self.setName(fullname)
        self.email = ""
        self.phone = ""

The above says, if fullname is given, use it in the call to setName(..). If fullname is not given, then just set the first and last name instance variables to the empty string.

This makes our Contact constructor just a little nicer, allowing two possible ways to call it:

    c2 = Contact()                      # fullname not given
    c2.setName("George Washington")
    print(c2)
    c3 = Contact("Martha Washington")   # fullname given, so use it
    print(c3)

Either way works now!

Also, note the call to self.setName(..) in the above constructor. If you are calling a method from the same class, the syntax is self.methodname(..).