Problem Solving with Algorithms and Data Structures using Python
Section 1.13: https://runestone.academy/ns/books/published/pythonds/Introduction/ObjectOrientedProgramminginPythonDefiningClasses.html
Section 2.1: https://runestone.academy/ns/books/published/pythonds/ProperClasses/a_proper_python_class.html
Section 3.6 (Big O of list operations): https://runestone.academy/ns/books/published/pythonds/AlgorithmAnalysis/Lists.html
Section 4.5, 4.6 (Stack implementation), 4.11, 4.12 (Queue Implementation): https://runestone.academy/ns/books/published/pythonds/BasicDS/toctree.html
Last time, we got our PlayingCard class to this point
class PlayingCard:
def __init__(self,v,s):
self.value = v
self.suit = s
def face(self):
if self.value == 11:
return "J"
elif self.value == 12:
return "Q"
elif self.value == 13:
return "K"
elif self.value == 14:
return "A"
else:
return str(self.value)
def __repr__(self):
return self.face()+str(self.suit)
def __lt__(self,other):
#return (self.value < other.value)
if self.value < other.value:
return True
else:
return False
two_of_clubs = PlayingCard(2,"♣")
two_of_hearts = PlayingCard(2,"♡")
ten_of_hearts = PlayingCard(10,"♡")
seven_of_spades = PlayingCard(7,"♠")
four_of_diamonds = PlayingCard(4,"♢")
jack_of_diamonds = PlayingCard(11,"♢")
print("Here's what the card looks like:",jack_of_diamonds)
if two_of_clubs < ten_of_hearts:
print("Player 2 wins the hand")
Here's what the card looks like: J♢ Player 2 wins the hand
When designing a class, you should plan for how to protect against accidental misuse.
For example, what happens if someone tries to do this?
pikachu = PlayingCard("Pikachu",40)
This particular class is only meant to represent standard French playing cards, so this should cause some kind of error.
One way to handle it might be like this:
class PlayingCard:
def __init__(self,v,s):
if (type(v) != int) or v > 14 or v < 2:
raise Exception("A PlayingCard's value must be an integer in the range 2-14.")
self.value = v
self.suit = s
twentyseven_of_clubs = PlayingCard(27,"♣")
pikachu = PlayingCard("Pikachu",40)
--------------------------------------------------------------------------- Exception Traceback (most recent call last) Input In [6], in <cell line: 10>() 7 self.value = v 8 self.suit = s ---> 10 twentyseven_of_clubs = PlayingCard(27,"♣") 11 pikachu = PlayingCard("Pikachu",40) Input In [6], in PlayingCard.__init__(self, v, s) 3 def __init__(self,v,s): 5 if (type(v) != int) or v > 14 or v < 2: ----> 6 raise Exception("A PlayingCard's value must be an integer in the range 2-14.") 7 self.value = v 8 self.suit = s Exception: A PlayingCard's value must be an integer in the range 2-14.
By convention, any attribute or method whose name starts with an underscore should be treated as private, meaning you shouldn't change the variable outside the class.
self._value
If it doesn't start with an underscore, it is public and changing it outside the class is fair game.
If you start it with two underscores, then Python performs name mangling, which doesn't let you change the name outside the class (unless you do something extra to get around the mangling).
self.__value
class PlayingCard:
def __init__(self,v,s):
if type(v) != type(1) or v > 14 or v < 2:
raise Exception("A PlayingCard's value must be an integer in the range 2-14.")
self.__value = v
self.__suit = s
def __repr__(self):
return str(self.__value)+str(self.__suit)
pikachu = PlayingCard(2,"♣")
pikachu.__value = "Pikachu" #this doesn't actually change self.__value
pikachu.__suit = 40
print(pikachu)
2♣
Update your class so that self.value and self.suit are hidden (private/mangled).
Container types are types meant to hold collections of other data - like lists, dictionaries, sets, and tuples.
We will create our own container classes in this course - it's one of the main thing this course is about.
Many custom container classes are built on top of existing containers, but by using data hiding and limited methods, they can control how the container can be used.
Let's create a Deck class
# Demo: let's define our Deck class here and create a method called put_on_top
# for placing new cards on the deck, and another called remove_from_top
# to remove a card from the top of the deck and return it
#Here's a working version for your notes.
class Deck:
def __init__(self):
self.__card_list = [] #the deck will be initially empty
def put_on_top(self,card):
self.__card_list.append(card)
def remove_from_top(self):
if len(self.__card_list) == 0:
raise Exception("This deck has no cards left.")
else:
return self.__card_list.pop()
two_of_clubs = PlayingCard(2,"♣")
ten_of_hearts = PlayingCard(10,"♡")
seven_of_spades = PlayingCard(7,"♠")
four_of_diamonds = PlayingCard(4,"♢")
my_deck = Deck()
my_deck.put_on_top(two_of_clubs)
my_deck.put_on_top(ten_of_hearts)
my_deck.put_on_top(seven_of_spades)
my_deck.put_on_top(four_of_diamonds)
print( my_deck.remove_from_top() )
print( my_deck.remove_from_top() )
print( my_deck.remove_from_top() )
4♢ 7♠ 10♡
Put your code for the PlayingCard and Deck into a file called carddeck.py, and implement the following methods for the Deck class:
__repr__()shuffle() - allows the deck to be mixed around in random order (if you're stuck, Google how to shuffle a list)is_empty() - returns True or False depending on if the deck is emptyIf you did it right, your PlayingCard and Deck classes should work with the following code, which is a simple game of high-card. On each turn, both players draw a card from the deck, and the one with the higher card value gets a point.
high_card_deck = Deck()
#create each of the 52 playing cards and put them in the deck
suits = ["♠","♣","♡","♢"]
for s in suits:
for v in range(2,15):
curr_card = PlayingCard(v,s)
high_card_deck.put_on_top(curr_card)
#look at the deck both before and after shuffling
print("Here's the pre-shuffled deck:",high_card_deck)
high_card_deck.shuffle()
print("Here's the deck after the shuffle:",high_card_deck)
#initialize both player's scores to 0
p1score = 0
p2score = 0
#keep going until all cards are dealt out
while not high_card_deck.is_empty():
#draw a card for each player
p1card = high_card_deck.remove_from_top()
p2card = high_card_deck.remove_from_top()
print("Player 1:",p1card,", Player 2:",p2card)
#check which player wins this hand
if p1card > p2card:
p1score += 1
print("Player 1 wins this hand.")
elif p1card < p2card:
p2score += 1
print("Player 2 wins this hand.")
else:
print("This hand is a draw.")
#Figure out who wins and display the game-end message
print("Player 1 score:",p1score,", Player 2 score:",p2score)
if p1score > p2score:
print("Player 1 wins the game!!!")
elif p2score > p1score:
print("Player 2 wins the game!!!")
else:
print("The game is a tie :(")
These are the following operations that all queues should have:
Queue() creates a new queue that is empty. It needs no parameters and returns an empty queue.enqueue(item) adds a new item to the back of the queue. It needs the item and returns nothing.dequeue() removes the item at the front of the queue. It needs no parameters and returns the item. The queue is modified.isEmpty() tests to see whether the queue is empty. It needs no parameters and returns a boolean value.size() returns the number of items in the queue. It needs no parameters and returns an integer.Remember: An abstract data type, sometimes abbreviated ADT, is a logical description of how we view the data and the operations that are allowed without regard to how they will be implemented.
Once we create a class that implements an ADT, we've created a Data Structure.
class Queue:
def __init__(self):
self.items = []
def isEmpty(self):
return self.items == []
def enqueue(self, item):
self.items.insert(0,item)
def dequeue(self):
return self.items.pop()
def size(self):
return len(self.items)
Test this code out with some code that uses a queue, and discuss whether it is working properly. Here's some code we used to test out queues before.
my_q = Queue()
my_q.enqueue(4)
my_q.enqueue(7)
my_q.enqueue(11)
my_q.dequeue()
my_q.enqueue(8)
my_q.dequeue()
my_q.enqueue(5)
my_q.enqueue(9)
print("Size:",my_q.size())
while not my_q.isEmpty():
print(my_q.dequeue())
Size: 4 11 8 5 9
One of the things that was annoying about working with stacks and queues from the pythonds module was that they didn't have a way to print them and see what values were in them. Add a __repr__ method to this Queue class which allows you to see what's in the queue.
Based on what you know about the Big O of different list operations, what are is the Big O of each of these Queue methods?
enqueuedequeuesizeFor reference, take a look at
Review: We've seen the textbook's description of the Stack ADT. Here it is.
These are the following operations that all stacks should have:
Stack() creates a new stack that is empty. It needs no parameters and returns an empty stack.push(item) adds a new item to the top of the stack. It needs the item and returns nothing.pop() removes the top item from the stack. It needs no parameters and returns the item. The stack is modified.peek() returns the top item from the stack but does not remove it. It needs no parameters. The stack is not modified.isEmpty() tests to see whether the stack is empty. It needs no parameters and returns a boolean value.size() returns the number of items on the stack. It needs no parameters and returns an integer.Create a class that implements this. It should be very similar to the Queue class.
What is the Big O of each of the following for your definition of a Stack?
pushpoppeeksizeDiscuss: What's the difference between the Deck class and the Stack class? Can a Deck also be considered a stack?
Discuss the following question, and make sure to write the answer in your notes: What is the difference between and Abstract Data Type and a Data Structure?