STAT 19000: Project 10 — Spring 2022

Motivation: We’d be remiss spending almost an entire semester solving data driven problems in python without covering the basics of classes. Whether or not you will ever choose to use this feature in your work, it is best to at least understand some of the basics so you can navigate libraries and other code that does use it.

Context: We’ve spent nearly the entire semester solving data driven problems using Python, and now we are going to learn about one of the primary features in Python: classes. Python is an object oriented programming language, and as such, much of Python, and the libraries you use in Python are objects which have attributes and methods. In this project we will explore some of the terminology and syntax relating to classes. This is the first in a series of 3 projects focused on reading and writing classes in Python.

Scope: Python, classes

Learning Objectives
  • Use classes to solve a data-driven problem.

  • Understand and identify attributes and methods of a class.

  • Differentiate between class attributes and instance attributes.

  • Differentiate between instance methods, class methods, and static methods.

Make sure to read about, and use the template found here, and the important information about projects submissions here.

Dataset(s)

The following questions will use the following dataset(s):

  • /depot/datamine/data/

Questions

Question 1

Carefully read through this quick walkthrough of classes in Python. In previous 190 projects, students built classes to represent decks of cards. Now, we’ve provided you with a couple of classes below.

class Card:

    _value_dict = {"2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7, "8":8, "9":9, "10": 10, "j": 11, "q": 12, "k": 13, "a": 14}
    def __init__(self, number, suit):
        if str(number).lower() not in [str(num) for num in range(2, 11)] + list("jqka"):
            raise Exception("Number wasn't 2-10 or J, Q, K, or A.")
        else:
            self.number = str(number).lower()
        if suit.lower() not in ["clubs", "hearts", "diamonds", "spades"]:
            raise Exception("Suit wasn't one of: clubs, hearts, spades, or diamonds.")
        else:
            self.suit = suit.lower()

    def __str__(self):
        return(f'{self.number} of {self.suit.lower()}')

    def __repr__(self):
        return(f'Card(str({self.number}), "{self.suit}")')

    def __eq__(self, other):
        if self.number == other.number:
            return True
        else:
            return False

    def __lt__(self, other):
        if self._value_dict[self.number] < self._value_dict[other.number]:
            return True
        else:
            return False

    def __gt__(self, other):
        if self._value_dict[self.number] > self._value_dict[other.number]:
            return True
        else:
            return False

    def __hash__(self):
        return hash(self.number)


class Deck:
    brand = "Bicycle"
    _suits = ["clubs", "hearts", "diamonds", "spades"]
    _numbers = [str(num) for num in range(2, 11)] + list("jqka")

    def __init__(self):
        self.cards = [Card(number, suit) for suit in self._suits for number in self._numbers]

    def __len__(self):
        return len(self.cards)

    def __getitem__(self, key):
        return self.cards[key]

    def __setitem__(self, key, value):
        self.cards[key] = value

There is a lot to unpack here, but don’t worry, we will cover it!

Instantiate, or create an instance of, the class Card called my_card. Do the same for the Deck class, calling the created object my_deck. Run the following.

print(my_card)
my_card

What are the differences in the output? Which parts of the Card class controls the appearance of the outputs? What are those two special methods called (something methods)?

Now run the following.

print(my_deck)

What is printed? Modify the Deck class so that it prints "A bicycle deck.", where "bicycle" would be changed to "copag" if the brand was changed to "copag".

Make sure that your modification works!

Items to submit
  • Code used to solve this problem.

  • Output from running the code.

Question 2

Okay great! You’ve already learned about one of the key types of methods in Python, and modified a class to fit your printing needs. Your friend is using your code at his company to track decks of cards their company uses. Previously, all of the decks of cards were Bicycle, however, they recently switched to Copag. Write a single line of code so that the brand is changed from "Bicycle" to "Copag" for both decks.

deck1 = Deck()
deck2 = Deck()
print(deck1)
print(deck2)
# add code here
print(deck1)
print(deck2)
expected output
A copag deck.
A copag deck.

Once you have that working as intended, explain what is going on. What type of attribute is brand? What happens if you did the same thing for the following code?

deck1 = Deck()
deck2 = Deck()
deck1.brand = "Aviator"
# add code to change both decks to "Copag"
print(deck1)
print(deck2)

Why does deck1 now remain as "Aviator" and deck2 as "Copag"?

This stackoverflow post may be useful?

Items to submit
  • Code used to solve this problem.

  • Output from running the code.

Question 3

Okay, you are now going to create a new class called a Player. This class will be used to represent a player in a game. A player should have the following features:

  • A deck to draw from.

  • A hand of cards.

  • A name of the player.

  • A draw method that draws a card from the deck and adds it to the hand.

Start by implementing the name attribute. Should the name attribute be a class attribute or an instance attribute? Why?

Next, implement the very important, init method. What arguments should be passed to the init method, and why?

There should be 3 arguments passed to the init method.

As long as the following code runs properly and gives you the expected output (of course, the second two outputs just need to be consistent; they don’t need to match our results), you are done with this problem. Great work!

my_deck = Deck()
# create player 1 here
player1 = ...
print(player1)
expected output
Chen Chen

Top 5 cards: [Card(str(2), "clubs"), Card(str(3), "clubs"), Card(str(4), "clubs"), Card(str(5), "clubs"), Card(str(6), "clubs")]
import random
# create player 2 here
player2 = ...
random.shuffle(my_deck)
print(player2)
expected output
Amy Sue

Top 5 cards: [Card(str(q), "hearts"), Card(str(7), "diamonds"), Card(str(5), "spades"), Card(str(4), "diamonds"), Card(str(7), "spades")]
print(player1)
expected output
Chen Chen

Top 5 cards: [Card(str(q), "hearts"), Card(str(7), "diamonds"), Card(str(5), "spades"), Card(str(4), "diamonds"), Card(str(7), "spades")]

We shuffled my_deck it makes sense that both players should then have a deck that is equivalently shuffled!

Make sure as you are updating the Player class, that you are running the code with the new updates to the class before using it.

Items to submit
  • Code used to solve this problem.

  • Output from running the code.

Question 4

Fantastic! Two common patterns that are important to be able to quickly recognize in many gin rummy games are sets and runs.

A set is a group of cards with different suits but the same value. In order to qualify as a set, there must be 3 or more cards.

A run is a group of cards with the same suit with sequential values. In order to qualify as a run, there must be 3 or more cards.

Before we can write code to see if a given player has a set or a run, we need to modify our Player class so our players have a hand attribute. For now, the hand attribute can just be a Python list. When the draw method is called, a card is removed from the "top" of the deck and appended to the hand list.

In addition, we need to write our first instance method — draw! This method doesn’t need to accept any arguments other than self, and it should simply remove one card from the deck and add it to the player’s hand. Not too bad! Make sure that the following code works.

The following code may be useful when trying to figure out how to remove a card from a deck.

print(len(my_deck))
card = my_deck.cards.pop(0)
print(card)
print(len(my_deck))
import random

fresh_deck = Deck()

player1 = Player("Dr Ward", fresh_deck)

# shuffle cards
random.shuffle(fresh_deck)

player1.draw()
print(player1.hand)

player1.draw()
print(player1.hand)

player1.draw()
print(player1.hand)

print(len(fresh_deck))
expected output
[Card(str(a), "diamonds")]
[Card(str(a), "diamonds"), Card(str(9), "clubs")]
[Card(str(a), "diamonds"), Card(str(9), "clubs"), Card(str(k), "clubs")]
49
Items to submit
  • Code used to solve this problem.

  • Output from running the code.

Question 5

Okay, great!

Add a new instance method to the Player class. It should be called has_set and should return True if the player has a set (in their hand), and False otherwise.

In the next project, we will discuss some ways to improve the functionality and implement more important features. For now, make sure that the following examples work.

Run the following code as many times as needed until the result is True. Once the result is True, print the hand to verify that the player has a set: print(player1.hand).

import random

my_deck = Deck()
random.shuffle(my_deck)
player1 = ... # create player 1 here
for _ in range(10): # player draws 10 cards from the deck
    player1.draw()

player1.has_set()
expected output (eventually)
True
print(player1.hand)
expected output
At least 3 cards with the same _value_.

The Counter function from the collections module may be useful here. For example.

from collections import Counter

my_list = [1, 1, 2, 3, 4]
my_result = Counter(my_list)

for key, value in my_result.items():
    print(key, value)
Items to submit
  • Code used to solve this problem.

  • Output from running the code.

Please make sure to double check that your submission is complete, and contains all of your code and output before submitting. If you are on a spotty internet connect ion, it is recommended to download your submission after submitting it to make sure what you think you submitted, was what you actually submitted.

In addition, please review our submission guidelines before submitting your project.