Cover page images (keyboard)

Defining Classes

David Riley & Sue Evans

Press space bar for next slide

Learning Objectives

Students will:

First things first...

What is a class?

So what's an object?

The data and methods are collectively called "attributes".

We've already seen objects provided to us by Python; for example, lists and strings are objects defined by the list and string classes built into Python.

An object example

So, for example, when we do the following:

>>> my_list = ["alice", "bob"]
>>> my_list.append("eve")
>>> print(my_list)
['alice', 'bob', 'eve']

We are first creating a list object with the initial items "alice" and "bob", then we are using the append() method of the list object to add "eve" to the list.

The methods of an object are typically the elements that compose its interface.

Some other method examples

Let's look a little more at the list class and some of its methods for other examples of how objects are created.

The list class contains many more methods than this, but these are good examples. Imagine having to write code to do all of this yourself; you'd have to worry about how the list is represented in memory, and you'd have to figure out a lot of operations (imagine the list.insert() method on a grocery list on paper (assuming there's no space between items); it's not fun).

Why use classes and objects?

It's not strictly necessary to use objects in programming; the concept of object-oriented programming is actually rather new. Programs used to be (and often still are) written entirely using functions. This is called "procedural" programming. However, many people find object-oriented programming to be a more intuitive way to program (for certain types of problems). Some reasons are:

The difference between classes and objects

It can be difficult to remember the difference between a class and an object. As we stated before:

So, for example, in a housing subdivision:

Instantiating objects

When we create an instance of a class, we instantiate it. We can instantiate objects that don't have explicit classes, too; for example, we can instantiate an integer or a float.

In Python, everything is an object (this is not true for many other languages). Therefore, everything must be instantiated.

Some objects can be instantiated in special ways; for example, there are several ways to create the same floating point number:

my_num1 = 3.0
my_num2 = float(3)
my_num3 = 7.0 / 2.0

  1. In the first case, a float (being a special object for the language) can be instantiated with just the number.
  2. The second case has us directly using the constructor to create the object (a constructor is a special method which creates the object. (You'll learn more about constructors later)
  3. In the third case, a float is implicitly returned as the result of the division of two other floats.

When we instantiate our own objects, we can generally only use the constructor method.

Defining your own classes

You'll eventually want to define your own classes to solve problems. Here's an example from the book of a problem neatly solved by utilizing object-oriented design.

Imagine we're writing a ballistic simulation where we must track the path of a shot fired from a catapult. The shot is affected by gravity, so we must simulate the effects of gravity on the shot. We need to perform the following operations:

We'll step through the procedural approach to this and then the object-oriented approach.

The procedural approach

We'll assume that the input has already been handled and that we have variables xpos and ypos for the initial position, where (xpos is the initial height, while ypos is just 0.0), xvel and yvel for the initial velocity, and timestep for the timestep (in seconds).

The position update function might look like this:

def updateShotPosition(xpos, ypos, xvel, yvel, timestep):
    xpos += xvel * timestep
    ypos += yvel * timestep
    return xpos, ypos

And the velocity update might look like this:

def updateShotVelocity(yvel, timestep):
    # Gravity is an acceleration of -9.8 meters/second^2.
    yvel -= 9.8 * timestep

    # Return the Y velocity
    # X velocity is constant since we're ignoring air resistance
    return yvel

Our main simulation loop would then look like this:

# Run until we hit the ground.
while ypos >= 0:
    # Update our positions based on the velocity and a timestep.
    xpos, ypos = updateShotPosition(xpos, ypos, xvel, yvel, timestep)

    # Update our velocity based on gravity and the same timestep.
    yvel = updateShotVelocity(yvel, timestep)
    
# Print our horizontal distance traveled.
print("Distance traveled: %0.1f meters." % (xpos))

That doesn't seem so bad, but it could be prettier. Let's look at the object-oriented way.

The object-oriented approach

We'll go over the details of actually defining the class on the next slide, but for now, let's think about how moving to objects simplifies things.

We'll also add some methods to access the position data in the object from the outside called Projectile.getX() and Projectile.getY() (we'll learn why in a bit).

We will instantiate an instance of our projectile before we start the main loop, initializing it with the initial position and velocity. This will look like:

shot = Projectile(xpos, ypos, xvel, yvel)

Given all that, our main loop (with an initialized Projectile object named shot) will look like:

while shot.getY() >= 0.0:
    shot.update(timestep)
    
print( "Distance traveled: %0.1f meters." % (shot.getX()) )

Defining the Projectile class

So how do we define the class? Our class definition will look like this:

class Projectile:
    def __init__(self, xpos, ypos, xvel, yvel):
        self.xpos = xpos
        self.ypos = ypos
        self.xvel = xvel
        self.yvel = yvel
        
    def getX(self):
        return self.xpos
        
    def getY(self):
        return self.ypos
        
    def update(self, timestep):
        # Update the position according to the velocity and timestep.
        self.xpos += self.xvel * timestep
        self.ypos += self.yvel * timestep
        
        # Update the velocity accordint to gravity and the timestep (the X
        # velocity is constant, so we don't change it).
        self.yvel -= 9.8 * timestep

Let's talk about the elements of this definition on the next slide.

The Constructor

shot = Projectile(xpos, ypos, xvel, yvel)

self

You'll notice that all the object's internal variables are prefixed with self, and all of the methods take a first parameter called "self", which is not explicitly passed to the methods when we call them.

The call to the method looks like this:

shot.update(timestep)

not

shot.update(shot, timestep)

nor

update(shot, timestep)

This variable is actually just the reference to the instance. So when you call shot.update(timestep), self is a reference to shot.

All object variables must be referenced through the self variable.

If you tried to access ypos instead of self.ypos in Projectile.update(), you would encounter an error, because ypos is neither a parameter nor a local variable in Projectile.update().

For example (using a simplified Projectile class):

>>> class Projectile:
...     def __init__(self):
...         self.xpos = 3.3
...     
...     def getX(self):
...         return xpos
... 
>>> shot = Projectile()
>>> shot.getX()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in getX
NameError: global name 'xpos' is not defined

Note that you could write to just xpos, but it would just go to a local variable and be destroyed once the method returns, which is probably not what you intended.

Internal object variables

Notice that the internal object variables are all defined inside the constructor. This is good programming practice.

You could add more variables in other methods, but it's not a good idea.

For example:

>>> class Human:
...     def __init__(self):
...         self.position = (0.0, 0.0)
...      
...     def setName(self, name):
...         self.name = name
...     
...     def describe(self):
...         print("Position:", self.position, "Name:", self.name)
... 
>>> frank = Human()
>>> frank.setName("Frank")
>>> frank.describe()
Position: (0.0, 0.0) Name: Frank
>>> bob = Human()
>>> bob.describe()
Position: (0.0, 0.0) Name:
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 9, in describe
AttributeError: Human instance has no attribute 'name'

Accessors & Mutators

Constructor advice

A little bit more about constructors -

Consider the following class for tracking student grades:

class Student:
    def __init__(self, name, credits, points):
        self.name    = name
        self.credits = credits
        self.points  = points
        
    def getName(self):
        return self.name
        
    def getCredits(self):
        return self.credits
    
    def getPoints(self):
        return self.points
        
    def gpa(self):
        return (self.points / self.credits)

Modules

We've seen modules before.

We can create our own modules, too, so that we can keep our class definitions in separate files. This eliminates clutter and makes it much easier to reuse your objects in later programs.

You don't have to do anything special to make a module besides putting the code in a separate file. Once you want to use your code in that file, you simply need to import the code file.

For example, if we had defined our Projectile class in a file called projectile.py, we would do the following to use the Projectile class:

>>> import projectile
>>> shot = projectile.Projectile(3.0, 0.0, 1.0, 5.0) 
>>> shot
<projectile.Projectile instance at 0x6b440>

We might find it a little more convenient to do it this way instead:

>>> from projectile import Projectile
>>> shot = Projectile(3.0, 0.0, 1.0, 5.0)
>>> shot
<projectile.Projectile instance at 0x6b6e8>

The "from <x> import <y>" approach is generally a little nicer with objects, but it restricts what we import from the module to what was specified.

If we had a whole lot of functions or constants in the module (e.g. math.sin() and math.pi), we might want to just import the module and live with the module prefix.

Module and class documentation

Documenting your programs with comments is a good practice in general. There is more we can do, however, to help users. Modules and objects have a special attribute called __doc__, known as the docstring, which can be examined at runtime. For example, if we wanted to get the dirt on the random.random() method:

>>> import random
>>> print(random.random.__doc__)
random() -> x in the interval [0, 1).

There is an even simpler way to get this info:

>>> import random
>>> help(random.random)

Help on built-in function random:

random(...)
    random() -> x in the interval [0, 1).

For something even more useful, try the above on your own computer, to get help on the entire random module (after importing random, just type "help(random)").

We can write our own docstrings for our modules, classes and methods. Simply include a string at the top of your definition:

class Student:
    """A class defining a student for the purpose of grade tracking."""
    
    def __init__(self, name, credits, points):
        """Initialize a student with given name, credit hours and quality
        points."""
        self.name = str(name)
        .
        .
        .

Cards Example

Now that we know about classes, I thought I'd like to try to write a Card class.

I eventually want to have this be graphical, but to start I'll be happy if I can just get it to work and print out the names of the cards that are being dealt.

from graphics import *
import string
import sys
import random

RANK_NAMES = {1:'Ace', 2:'Two', 3:'Three', 4:'Four', 5:'Five', 
6:'Six', 7:'Seven', 8:'Eight', 9:'Nine', 10:'Ten', 11:'Jack', 
12:'Queen', 13:'King'}

SUIT_NAMES = {'s':'spades', 'c':'clubs', 'h':'hearts', 'd':'diamonds'}


class Card:

    # Constructor
    def __init__(self, rank, suit):
        self.rank = int(rank)
        self.suit = str(suit)
        
        if self.rank == 1:
            self.bJPts = 1
            self.rummyPts = 15
        elif self.rank > 1 and self.rank < 10:
            self.bJPts = self.rank
            self.rummyPts = 5
        else:
            self.bJPts = self.rummyPts = 10

        self.name = RANK_NAMES.get(self.rank) + ' of '
        self.name = self.name + self.suit

        self.filename = str(self.rank) + self.suit + ".GIF"

    # Accessors
    def getRank(self):
        return self.rank

    def getSuit(self):
        return self.suit

    def getBJPts(self):
        return self.bJPts

    def getRummyPts(self):
        return self.rummyPts

    def getName(self):
        return self.name

    def getFilename(self):
        return self.filename



# dealHand(hand, deck, numCards) deals numCards
# from the deck into the hand
#
# Inputs: hand, an empty list to put cards into
#         deck, the remaining undealt cards
#         numCards, the number of cards to be
#                   dealt into this hand
# Outputs: None, but hand and deck will be modified
def dealHand(hand, deck, numCards):

    deckSize = len(deck)
    if deckSize < numCards:
        print("There are only", deckSize, "cards left in the deck")
        print("So I can't deal a hand of", numCards, "cards")
        sys.exit()
        
    for i in range(numCards):
        hand.append(deck[i])
        del(deck[i])


def main():

    deck = []
    numRanks = len(RANK_NAMES)

    # Make a deck of cards
    for suit in SUIT_NAMES:
        for rank in range(1, numRanks + 1):
            deck.append(Card(rank, SUIT_NAMES.get(suit)))

    print("\nLet's play cards!\n")
    
    # Shuffle it
    print("I'm shuffling the deck.\n")
    random.shuffle(deck)

    # Get number of cards for this hand
    numCardsPerHand = int(input("How many cards would you like ? "))
    
    hand = []
    dealHand(hand, deck, numCardsPerHand)

    # Print their names
    numCards = len(hand)
    for i in range(numCards):
        name = Card.getName(hand[i])
        print(name)

main()

Let's run it!
linuxserver1.cs.umbc.edu[109] python cards.py

Let's play cards!

I'm shuffling the deck.

How many cards would you like ? 5
Ace of clubs
Seven of clubs
Two of spades
Nine of spades
Ten of diamonds
linuxserver1.cs.umbc.edu[110] python cards.py

Let's play cards!

I'm shuffling the deck.

How many cards would you like ? 5
Eight of clubs
Jack of clubs
Five of hearts
King of diamonds
Two of clubs

That figures! Two poker hands and the best I get is Ace-high.

Let's work on the graphical version

from graphics import *
import string
import sys
import random
import time

RANK_NAMES = {1:'Ace', 2:'Two', 3:'Three', 4:'Four', 5:'Five', 
6:'Six', 7:'Seven', 8:'Eight', 9:'Nine', 10:'Ten', 11:'Jack', 
12:'Queen', 13:'King'}

SUIT_NAMES = {'s':'spades', 'c':'clubs', 'h':'hearts', 'd':'diamonds'}


class Card:

    # Constructor
    def __init__(self, rank, suit):
        self.rank = int(rank)
        self.suit = str(suit)
        
        if self.rank == 1:
            self.bJPts = 1
            self.rummyPts = 15
        elif self.rank > 1 and self.rank < 10:
            self.bJPts = self.rank
            self.rummyPts = 5
        else:
            self.bJPts = self.rummyPts = 10

        self.name = RANK_NAMES.get(self.rank) + ' of '
        self.name = self.name + self.suit

        self.filename = str(self.rank) + self.suit + ".GIF"

    # Accessors
    def getRank(self):
        return self.rank

    def getSuit(self):
        return self.suit

    def getBJPts(self):
        return self.bJPts

    def getRummyPts(self):
        return self.rummyPts

    def getName(self):
        return self.name

    def getFilename(self):
        return self.filename



# dealHand(hand, deck, numCards) deals numCards
# from the deck into the hand
#
# Inputs: hand, an empty list to put cards into
#         deck, the remaining undealt cards
#         numCards, the number of cards to be
#                   dealt into this hand
# Outputs: None, but hand and deck will be modified
def dealHand(hand, deck, numCards):

    deckSize = len(deck)
    if deckSize < numCards:
        print("There are only", deckSize, "cards left in the deck")
        print("So I can't deal a hand of", numCards, "cards")
        sys.exit()
        
    for i in range(numCards):
        hand.append(deck[i])
        del(deck[i])


def main():

    deck = []
    numRanks = len(RANK_NAMES)

    # Make a deck of cards
    for suit in SUIT_NAMES:
        for rank in range(1, numRanks + 1):
            deck.append(Card(rank, SUIT_NAMES.get(suit)))

    print("\nLet's play cards!\n")
    
    # Shuffle it
    print("I'm shuffling the deck.\n")
    random.shuffle(deck)

    # Get number of cards for this hand
    numCardsPerHand = int(input("How many cards would you like ? "))
    
    hand = []
    dealHand(hand, deck, numCardsPerHand)

    # Show them to the player
    numCards = len(hand)
    
    win = GraphWin("Let's Play Cards!", numCards * 100, 300)
    win.setBackground("green3")
    x = 50
    y = 100
    
    for i in range(numCards):
        pic = Image(Point(x, y), Card.getFilename(hand[i]))
        x += 100
        pic.draw(win)

    time.sleep(10)
    win.close()


main()

Let's see if my luck has changed!

Class Exercise

Write a class to represent a geometric solid cube.