David Riley & Sue Evans
Press space bar for next slide
Students will:
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.
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.
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).
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:
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:
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
When we instantiate our own objects, we can generally only use the constructor method.
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.
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.
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()) )
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.
shot = Projectile(xpos, ypos, xvel, yvel)
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.
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'
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)
class Student: def __init__(self, name, credits, points): self.name = str(name) self.credits = float(credits) self.points = float(points) . . .
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.
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) . . .
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()
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!
Write a class to represent a geometric solid cube.