Hiding Attributes and Container Classes¶

CS 66: Introduction to Computer Science II¶

References for this lecture¶

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

Picking up the example from last time¶

Last time, we got our PlayingCard class to this point

In [1]:
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

Controlling Access¶

When designing a class, you should plan for how to protect against accidental misuse.

For example, what happens if someone tries to do this?

In [ ]:
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:

In [6]:
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.

Hiding Attributes¶

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

In [14]:
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♣

Group Activity Problem 1¶

Update your class so that self.value and self.suit are hidden (private/mangled).

Container Classes¶

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

  • purpose: keep track of a collection of playing cards
  • build on top of a list
  • only allow cards to be added and removed from the "top" of the deck
In [8]:
# 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
In [9]:
#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♡

Why is this better than just using a list?¶

  • We know that we won't accidentally insert a card on the bottom or middle of the deck - it's safer for the rest of our code.
  • We can add custom methods to do things that lists don't do

Group Activity Problem 2¶

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 empty

If 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.

In [ ]:
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 :(")

Review: The Queue Abstract Data Type¶

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.

Implementing the Queue¶

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.

In [10]:
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)

Group Activity Problem 3¶

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.

In [11]:
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

Group Activity Problem 4¶

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.

Group Activity Problem 5¶

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?

  • enqueue
  • dequeue
  • size

For reference, take a look at

  • the section of the book on the computational complexity of list operations: https://runestone.academy/ns/books/published/pythonds/AlgorithmAnalysis/Lists.html
  • the official Python wiki: https://wiki.python.org/moin/TimeComplexity

Group Activity Problem 6¶

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.

Group Activity Problem 7¶

What is the Big O of each of the following for your definition of a Stack?

  • push
  • pop
  • peek
  • size

Group Activity Problem 8¶

Discuss: What's the difference between the Deck class and the Stack class? Can a Deck also be considered a stack?

Group Activity Problem 9¶

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?