Classes and Defining New Types¶

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 2.2 appears to be missing from the book, but we'll cover that too

Defining new types¶

In Python, we use classes to create new types

A class defines two things:

  1. Data/Attributes: what do objects of this type look like?
  2. Methods: what can you do with objects of this type?

Example: Date object¶

Let's look at the date type

The date class is defined in the datetime module, and we can import it and use it in our code

In [1]:
import datetime

#creating a new date object
decl_ind_date = datetime.date(1776,7,4) 

#datetime.date is a type
print( type(decl_ind_date) )

print("Here's what it looks like when we print a date:",decl_ind_date)

print("Here's what the data that makes up a date looks like:")
print( decl_ind_date.month )
print( decl_ind_date.day )
print( decl_ind_date.year )

#you can call methods on dates - here's one thing you can do with a date
#weekday method returns the number of the day of the week this date fell on (0 = Monday, 6 = Sunday)
print( decl_ind_date.weekday() ) 
<class 'datetime.date'>
Here's what it looks like when we print a date: 1776-07-04
Here's what the data that makes up a date looks like:
7
4
1776
3

month, day, and year are attributes - which data values associated with the object

weekday() is a method - like a function, but you call it using dot notation on a date object

How do we write our own classes?¶

Classes allow you to encapsulate data and actions-on-that-data together into one thing - this is an abstraction technique - it's good programming.

A class defines how objects behave - it is a blueprint that can be used to create many different objects of that type

Syntax:

  • keyword class
  • a name you decide (by convention, start with uppercase letter)
  • a colon :
  • indented list of function definitions (i.e., method definitions)
    • each method has a parameter called self which refers to the particular object being used at that time
In [2]:
class Motivator:
    
    def message1(self):
        print("You can do it!")
        
    def message2(self):
        print("I'm proud of you!")
In [3]:
m = Motivator()
m.message2()
print( type(m) )
I'm proud of you!
<class '__main__.Motivator'>

Notice that you always have to make self a parameter, but you don't send it as an argument in parantheses like other arguments.

self is the object (here, m) that the method was called on

Defining Classes with Attributes¶

Any attribute can be accessed in any of the class's methods using self. Each object of the class has a different set of all the attributes (just like different date objects represent different dates on the calendar)

Initialize attributes using the special __init__() method, which will be invoked whenever a new object of this type is created.

In [4]:
class PersonalMotivator:
    
    def __init__(self,n):
        self.name = n
    
    def message1(self):
        print("You can do it,",self.name)
        
    def message2(self):
        print("I'm proud of you,",self.name)
In [5]:
#creates two objects of the PersonalMotivator class
eric_motivator = PersonalMotivator("Eric")
tim_motivator = PersonalMotivator("Tim")


eric_motivator.message1()
eric_motivator.message2()
tim_motivator.message1()
You can do it, Eric
I'm proud of you, Eric
You can do it, Tim

Group Activity Problem 1:¶

  • Where does self.name get its value from?
  • When I call eric_motivator.message2(), what is self?
  • How would I create a third object of the PersonalMotivator class? Do I have to pass it a name?

Rectangle class example¶

In [6]:
class Rectangle:
    """
    Used for representing rectangles
    
    attributes: length, width
    """
    def __init__(self, l, w):
        self.length = l
        self.width = w
        
    def area(self):
        return self.length*self.width
    
    def perimeter(self):
        return 2*self.length + 2*self.width
In [7]:
rec1 = Rectangle(5, 10) #instantiates a new object of type Rectangle
rec2 = Rectangle(2, 3) #instantiates a new object of type Rectangle
print("Rectangle 1's area:", rec1.area() ) # self is rec1 here
print("Rectangle 1's perimeter:", rec1.perimeter() )
print("Rectangle 2's area:", rec2.area() ) # self is rec2 here
print("Rectangle 2's perimeter:", rec2.perimeter() )
Rectangle 1's area: 50
Rectangle 1's perimeter: 30
Rectangle 2's area: 6
Rectangle 2's perimeter: 10

Notice:¶

I have multiple rectangle objects but only one class definition

A class is a blueprint for creating many objects - it's like how you can build many houses in a neighborhood from one set of blueprints.

Object-oriented programming: a popular style of programming that centers on creating custom classes and objects instantiated from those classes.

A Bare-bones feet-inches example class¶

Let's say we are creating a program that uses a lot of measurements in feet-inches (e.g., numbers like 5 ft. 6 in. or 5'6") - maybe it is a drafting program for planning home remodeling projects.

To create a basic feet-inches type, we might start with a class like this:

In [8]:
class FeetInches:
    
    def __init__(self,f,i):
        self.feet = f
        self.inches = i
        
    def simplify(self):
        """
        if the number of inches is > 12, 
        this regroups the excess into feet
        """
        self.feet += self.inches // 12
        self.inches = self.inches % 12
    
    def display(self):
        print(self.feet,"ft.",self.inches,"in.")
        
room_length = FeetInches(12,4)
room_width = FeetInches(9,15)

#displayinig the room dimensions
room_length.display()
room_width.display()

#simplifying the dimensions and then displaying them
room_length.simplify()
room_width.simplify()

room_length.display()
room_width.display()
12 ft. 4 in.
9 ft. 15 in.
12 ft. 4 in.
10 ft. 3 in.

Controlling what the data type "looks like"¶

Having a display() function to nicely print the measurement isn't very flexible. I'd like to treat it like any other data type and just do something like

In [9]:
print("The length of the room is",room_length)
The length of the room is <__main__.FeetInches object at 0x10662c100>

Fortunately, Python allows us to write a special method __repr__() where you can define what the object looks like with a string representation.

This will work with things like print(), str(), and how it displays the object in the interactive shell.

The __repr__() method should return a string representation of the object (not print it).

In [10]:
class FeetInches:
    
    def __init__(self,f,i):
        self.feet = f
        self.inches = i
        
    def simplify(self):
        """
        if the number of inches is > 12, 
        this regroups the excess into feet
        """
        self.feet += self.inches // 12
        self.inches = self.inches % 12
    
    def __repr__(self):
        return str(self.feet)+"ft. "+str(self.inches)+"in."
        
        
room_length = FeetInches(12,4)
room_width = FeetInches(9,15)

#simplifying the dimensions
room_length.simplify()
room_width.simplify()

#displayinig the room dimensions
print("The length of the room is",room_length)
print("The width of the room is",room_width)
str(room_length) #now I can convert it to a string
The length of the room is 12ft. 4in.
The width of the room is 10ft. 3in.
Out[10]:
'12ft. 4in.'

Using operators with our new type¶

Unfortunately, our new type doesn't let us do things like this:

In [11]:
room_length = FeetInches(12,4)
room_width = FeetInches(9,15)

room_length + room_width
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Input In [11], in <cell line: 4>()
      1 room_length = FeetInches(12,4)
      2 room_width = FeetInches(9,15)
----> 4 room_length + room_width

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

Group Activity Problem 2¶

Based on what we've covered already from classes, how could we support being able to add two FeetInches values?

One solution might be to just implement an add() function like this:

In [12]:
class FeetInches:
    
    def __init__(self,f,i):
        self.feet = f
        self.inches = i
        
    def simplify(self):
        """
        if the number of inches is > 12, 
        this regroups the excess into feet
        """
        self.feet += self.inches // 12
        self.inches = self.inches % 12
    
    def __repr__(self):
        return str(self.feet)+"ft. "+str(self.inches)+"in."
    
    def add(self,other_measurement):
        total_feet = self.feet + other_measurement.feet
        total_inches = self.inches + other_measurement.inches
        
        #create an new FeetInches object with the new measurements
        total_FI = FeetInches(total_feet,total_inches)
        total_FI.simplify()
        return total_FI
    

measurement1 = FeetInches(3,6)
measurement2 = FeetInches(2,6)
total = measurement1.add(measurement2)
print(total)
6ft. 0in.

A better way¶

However, we can instead implement the special __add__() method, which will make it work with the + operator.

In [13]:
class FeetInches:
    
    def __init__(self,f,i):
        self.feet = f
        self.inches = i
        
    def simplify(self):
        """
        if the number of inches is > 12, 
        this regroups the excess into feet
        """
        self.feet += self.inches // 12
        self.inches = self.inches % 12
    
    def __repr__(self):
        return str(self.feet)+"ft. "+str(self.inches)+"in."
    
    def __add__(self,other_measurement):
        total_feet = self.feet + other_measurement.feet
        total_inches = self.inches + other_measurement.inches
        
        #create an new FeetInches object with the new measurements
        total_FI = FeetInches(total_feet,total_inches)
        total_FI.simplify()
        return total_FI
    

measurement1 = FeetInches(3,6)
measurement2 = FeetInches(2,6)
total = measurement1 + measurement2
print(total)
6ft. 0in.

Similarly, you can support any of the following operators (plus more that I haven't listed):

  • +: object.__add__(self, other)
  • -: object.__sub__(self, other)
  • *: object.__mul__(self, other)
  • /: object.__truediv__(self, other)
  • //: object.__floordiv__(self, other)
  • %: object.__mod__(self, other)
  • **: object.__pow__(self, other)
  • <<: object.__lshift__(self, other)
  • >>: object.__rshift__(self, other)
  • &: object.__and__(self, other) (this is not the logical and operator)
  • ^: object.__xor__(self, other)
  • |: object.__or__(self, other) (this is not the logical or operator)
  • <: object.__lt__(self, other)
  • <=: object.__le__(self, other)
  • ==: object.__eq__(self, other)
  • !=: object.__ne__(self, other)
  • >: object.__gt__(self, other)
  • >=: object.__ge__(self, other)

Group Activity Problem 3¶

Add support for the subtraction operator to the FeetInches class.

Group Activity Problem 4¶

Discussion the following questions.

  • What is the name of the function would I have to define (i.e., like __add__ is for +) to be able to compare two FeetInches objects like in the code below?
  • Note that __add__ and __sub__ both return a new object of type FeetInches. What type of value should the function for < return?
In [ ]:
measurement1 = FeetInches(3,6)
measurement2 = FeetInches(2,6)
print(measurement1 < measurement2)

Group Activity Problem 5¶

Add support for the < (less than) operator to the FeetInches class.

Group Activity Problem 6¶

After you implement <, you might get some other operators like > for free. Try the code below and see if it works with your definition. Do <=, ==, !=, >= work? Why or why not?

In [ ]:
measurement1 = FeetInches(3,6)
measurement2 = FeetInches(2,6)
print(measurement1 > measurement2)