Classes#

Classes are the fundamental concept for object oriented programming. A class defines a data type with both data and functions that can operate on the data. An object is an instance of a class. Each object will have its own namespace (separate from other instances of the class and other functions, etc. in your program).

We use the dot operator, ., to access members of the class (data or functions). We’ve already been doing this a lot, strings, ints, lists, … are all objects in python.

A simple class#

Here’s a class that holds some student info

class Student:
    def __init__(self, name, grade=None):
        self.name = name
        self.grade = grade
a = Student("Mike")
print(a.name)
print(a.grade)
Mike
None

Let’s create a bunch of them, stored in a list

students = []
students.append(Student("fry", "F-"))
students.append(Student("leela", "A"))
students.append(Student("zoidberg", "F"))
students.append(Student("hubert", "C+"))
students.append(Student("bender", "B"))
students.append(Student("calculon", "C"))
students.append(Student("amy", "A"))
students.append(Student("hermes", "A"))
students.append(Student("scruffy", "D"))
students.append(Student("flexo", "F"))
students.append(Student("morbo", "D"))
students.append(Student("hypnotoad", "A+"))
students.append(Student("zapp", "Q"))

Exercise

Loop over the students in the students list and print out the name and grade of each student, one per line.

We can use list comprehensions with our list of objects. For example, let’s find all the students who have A’s

As = [q.name for q in students if q.grade.startswith("A")]
As
['leela', 'amy', 'hermes', 'hypnotoad']

Playing Cards#

here’s a more complicated class that represents a playing card. Notice that we are using unicode to represent the suits.

class Card:
    
    def __init__(self, suit=1, rank=2):
        if suit < 1 or suit > 4:
            print("invalid suit, setting to 1")
            suit = 1
            
        self.suit = suit
        self.rank = rank
        
    def value(self):
        """ we want things order primarily by rank then suit """
        return self.suit + (self.rank-1)*14
    
    # we include this to allow for comparisons with < and > between cards 
    def __lt__(self, other):
        return self.value() < other.value()

    def __eq__(self, other):
        return self.rank == other.rank and self.suit == other.suit
    
    def __repr__(self):
        return self.__str__()
    
    def __str__(self):
        suits = [u"\u2660",  # spade
                 u"\u2665",  # heart
                 u"\u2666",  # diamond
                 u"\u2663"]  # club
        
        r = str(self.rank)
        if self.rank == 11:
            r = "J"
        elif self.rank == 12:
            r = "Q"
        elif self.rank == 13:
            r = "K"
        elif self.rank == 14:
            r = "A"
                
        return r +':'+suits[self.suit-1]

When you instantiate a class, the __init__ method is called. Note that all method in a class always have “self” as the first argument – this refers to the object that is invoking the method.

we can create a card easily.

c1 = Card()

We can pass arguments to __init__ in when we setup the class:

c2 = Card(suit=2, rank=2)

Once we have our object, we can access any of the functions in the class using the dot operator

c2.value()
16
c3 = Card(suit=0, rank=4)
invalid suit, setting to 1

The __str__ method converts the object into a string that can be printed.

print(c1)
print(c2)
2:♠
2:♥

the value method assigns a value to the object that can be used in comparisons, and the __lt__ method is what does the actual comparing

print(c1 > c2)
print(c1 < c2)
False
True

Note that not every operator is defined for our class, so, for instance, we cannot add two cards together:

c1 + c2
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[12], line 1
----> 1 c1 + c2

TypeError: unsupported operand type(s) for +: 'Card' and 'Card'

Exercise

  • Create a “hand” corresponding to a straight (5 cards of any suite, but in sequence of rank)

  • Create another hand corresponding to a flush (5 cards all of the same suit, of any rank)

  • Finally create a hand with one of the cards duplicated—this should not be allowed in a standard deck of cards. How would you check for this?

Operators#

We can define operations like + and - that work on our objects. Here’s a simple example of currency—we keep track of the country and the amount

class Currency:
    """ a simple class to hold foreign currency """
    
    def __init__(self, amount, country="US"):
        self.amount = amount
        self.country = country
        
    def __add__(self, other):
        return Currency(self.amount + other.amount, country=self.country)

    def __sub__(self, other):
        return Currency(self.amount - other.amount, country=self.country)

    def __str__(self):
        return f"{self.amount} {self.country}"

We can now create some monetary amounts for different countries

d1 = Currency(10, "US")
d2 = Currency(15, "US")
print(d2 - d1)
5 US

Exercise

As written, our Currency class has a bug—it does not check whether the amounts are in the same country before adding. Modify the __add__ method to first check if the countries are the same. If they are, return the new Currency object with the sum, otherwise, return None.