Week 13: More Classes and Objects
Week 13 Goals
-
Gain more practice writing classes
-
Import classes and use instances of one class to build another
Week 13 Files
-
card.py
: ACard
class for playing card-based games. -
deck.py
: ADeck
class that represents a deck of cards. -
blackjack_hand.py
: ABlackjackHand
class that represents one hand in a game of blackjack. -
blackjack.py
: Full implementation of a game of blackjack. -
bankaccount.py
: ABankAccount
class that shows how one object can operate on another instance of the same class.
Recap of terminology and key concepts
-
A class is a template for a new type.
-
Every object is an instance of some class.
-
The data within a class is stored in instance variables (using
self.<variablename>
). -
The operations within a class are called methods and always include self as their first parameter.
-
The
__init__
method is the constructor. The constructor creates an instance of the class. -
The
__str__
method is called when to convert your object into a string. This method should return a string, not print. -
getter methods allow you to access information within the object.
-
The syntax
if __name__ == "__main__":
at the bottom of the file that contains the class definition allows you to test the class while making sure that code doesn’t run when you import the class into another file.
Relationship Between Classes
One of the powerful things about object-oriented programming and being able to define our own custom classes is that we can aggregate them into more complex structures.
In this example, we’ll create a class Deck
which represents a collection of
Card
objects.
First, here’s the definition of the Card
class:
Card
-
Constructor: construct a new card
-
parameters: rank, suit (strings)
-
later we will try to enforce that users only provide legal values
class Card: """ Represents a playing card with a suit and a rank """ def __init__(self, rank, suit): """ Construct a new card Parameters: rank and suit should be strings """ self.rank = rank self.suit = suit
-
-
String method:
-
return a string representing the card
-
use T instead of 10 so it looks nicer when printing
-
we can try to use emojis (
import emoji
)def __str__(self): """ Create a string representation of the card """ suit = self.suit rank = self.rank if rank == '10': rank = 'T' # make the "10" 1-character wide in the display return "%s of %s" % (rank, suit)
-
-
Getters:
-
get_rank
,get_suit
-
get_value
(some card games return values for each card) where A=1 and K,Q,J=10def get_rank(self): return self.rank def get_suit(self): return self.suit def get_value(self): """ Some card games assign values to each rank """ if self.rank == 'A': return 1 elif self.rank in ['K', 'Q', 'J']: return 10 else: return int(self.rank)
-
Deck
Now we can create a class Deck
that includes a list of Card
objects as one
of its instance variables. A Deck
is going to contain all 52 of the possible
cards.
-
data:
-
cards (list of Card objects)
-
-
methods:
-
shuffle
-
deal
-
reset_deck (put all the cards back in the Deck)
-
Since we’re going to make the Deck
out of Card
objects, we should import the
Card
class into our file.
from card import Card
from random import shuffle
class Deck:
""" Represents a standard deck of 52 playing cards """
def __init__(self):
""" Calls method to create a list of all possible cards """
self.reset_deck()
def __str__(self):
""" Returns a string representing the entire deck """
result = ""
for i in range(len(self.cards)):
result += "%3s " % (self.cards[i])
return result
def __len__(self):
""" The length of a Deck is the number of cards in the deck """
return len(self.cards)
def reset_deck(self):
""" Resets the deck with all 52 possible cards """
self.cards = []
ranks = ['A','2','3','4','5','6','7','8','9','10','J','Q','K']
suits = ['hearts', 'clubs', 'diamonds', 'spades']
for suit in suits:
for rank in ranks:
self.cards.append(Card(rank, suit))
def shuffle(self):
""" Shuffles the order of the cards in the deck """
shuffle(self.cards)
def deal(self):
""" Deals out the top card from the deck """
return self.cards.pop(0)
-
The constructor has one line:
self.reset_deck
-
This method will allow us to reset the deck.
-
-
reset_deck
-
Creates a new list of all 52 cards
-
Creates
self.cards
which is an empty list -
Outer loop for each suit, inner loop for each rank
-
Create a new Card and append to
self.cards
-
-
__str__
-
Build up a string with multiple lines (if necessary)
-
-
__len__
-
The
__len__
method gets called when you calllen()
on an object
-
-
shuffle
-
Use the random library to shuffle our cards
-
-
deal
-
remove and return the first card using the
list.pop
method
-
Blackjack Classes
Our next goal is to use the Deck
and Card
classes to build a blackjack
game.
Rules
-
Played at casinos and usually involves betting
-
Players play against the dealer
-
Each player, including the dealer, starts with 2 cards
-
A player can choose to draw or hold
-
The goal for every player is to get as high a score as possible while still being less than or equal to 21.
-
The score is based on the ranks of the cards in your hand
-
2 is worth 2, 3 is worth 3, and so on
-
Face cards (J,Q,K) are worth 10
-
Ace is worth 1 or 11 depending on what is more beneficial to the player
-
-
If you get to 21 exactly, this is called "blackjack"
-
If you go over 21, you "bust" and lose
-
For our version of the game, the dealer has an algorithm that it follows:
-
If the dealer has fewer than 17 points, it must draw
-
If the dealer has 17 or more points, it must hold
-
We will play a simplified version with no betting.
We will play multiple rounds until the user chooses to quit.
BlackjackHand Class
We’ll start by defining a new class called BlackjackHand
in the file
blackjack_hand.py
. This class will represent one hand in a game of
blackjack. It should support the following methods:
-
__str__
-
Build up and return a string to produce a human-readable representation of this blackjack hand.
-
-
__len__
-
The
__len__
method gets called when you calllen()
on an object. For a blackjack hand, it should return the number of cards in the hand.
-
-
get_name
-
Return the name of the hand’s player.
-
-
add_card
-
Add a card to the hand (the card to add is provided as a parameter).
-
-
get_value
-
Produce and return a total score (an integer) that this hand represents according to the rules above.
-
2-10: value is 2-10
-
JQK: value is 10
-
A is 1 or 11
-
We want to return the highest value that (if possible) doesn’t exceed 21.
-
First we sum up all the cards assuming Ace is 11
-
As long as we have more than a score of 21 and some Aces we are still considering 11, consider one of the Aces as 1
-
-
Blackjack Game Program
Now we can finally implement the blackjack game: a conventional program with functions!
It uses the Deck
class and the BlackjackHand
class and it’s going to
implement a blackjack game.
The general design of the game is:
-
Show instructions
-
Get player’s name
-
Create and shuffle a deck of cards
-
Continue until the user quits:
-
player takes their turn
-
deal two cards to the player
-
ask them to draw or hold and verify input
-
if player has more than 21, they lose and the dealer doesn’t have to go
-
-
dealer takes their turn
-
deal two cards to the dealer
-
if dealer has more than 21, they lose
-
If the dealer has fewer than 17 points, it must draw
-
If the dealer has 17 or more points, it must hold
-
-
increment appropriate player_wins or dealer_wins counter
-
print appropriate message ("Player wins!", "Dealer wins!")
-
ask if they want to quit
-
-
Show final stats
Having One Class Use an Instance of the Same Class
In the previous example, we saw that one class used an instance of another, e.g. the Deck
and BlackjackHand
classes had lists of Card
objects.
But we can also have an object use an instance of the same class.
For instance, let’s say we have a BankAccount
class like this:
"""
Class to represent a bank account.
"""
class BankAccount:
def __init__(self, name):
"""
Constructor to create new BankAccount instance.
In addition to initializing the name, the BankAccount's
balance is initialized to 0
"""
self.name = name
self.balance = 0.0
def getName(self):
return self.name
def getBalance(self):
return self.balance
def deposit(self, amount):
"""
Attempts to make a deposit to the BankAccount.
Increases the balance by the amount, assuming the amount is positive.
Otherwise, does not modify the balance.
Parameters:
* self: refers to this object
* amount (float): amount to be deposited
Returns (boolean): whether or not the balance was modified.
"""
if (amount <= 0):
return False
else:
self.balance = self.balance + amount
return True
def withdraw(self, amount):
"""
Attempts to withdraw from the BankAccount.
Decreases the balance by the amount, assuming the amount is positive
but is not greater than the balance (i.e. the resulting balance
cannot be negative).
Otherwise, does not modify the balance.
Parameters:
* self: refers to this object
* amount (float): amount to be withdrawn
Returns (boolean): whethe or not the balance was modified.
"""
if (amount <= 0 or amount > self.balance):
return False
else:
self.balance = self.balance - amount
return True
def transfer(self, amount, other):
"""
Attempts to transfer money from this BankAccount to the other one.
Will only do so if the amount is positive and is not greater than
this object's balance.
Otherwise, the balances are not modified.
Parameters:
* self: refers to this object
* amount (float): amount to be transferred
* other (BankAccount): account to which money should be transferred
Returns (boolean): whethe or not transfer was made.
"""
if (amount <= 0):
return False
elif (amount > self.balance):
return False
else:
self.withdraw(amount)
other.deposit(amount)
return True
Note that the transfer
method takes a parameter of another instance of the BankAccount
class. This is okay, because this method can differentiate between the object on which it is called (self
) and the one that is passed as an argument (other
).
So now we can transfer money from one BankAccount
to another like this:
def main():
b1 = BankAccount("Bingo")
b1.deposit(100)
b2 = BankAccount("Bluey")
b2.deposit(400)
b1.transfer(50, b2) # transfers from b1 to b2
print(b1.getBalance()) # Bingo should now have $50
print(b2.getBalance()) # Bluey should now have $450