In-Class Lecture 17

The questions below are due on Tuesday April 09, 2024; 11:59:00 PM.
 
You are not logged in.

Please Log In for full access to the web site.
Note that this link will take you to an external site (https://shimmer.mit.edu) to authenticate, and then you will be redirected back to this page.

Download Materials

Summary

Learning Objectives (click to reveal)
  • An object is made up of two kinds of attributes: data and procedures
  • An object can be represented in many different ways
  • Create a Python class in code
  • Set up data attributes in a Python class
  • Add methods to a Python class
  • Understand the meaning behind self in the context of data attributes and methods
  • The meaning of dot notation
Takeaways (click to reveal)
Object Oriented Programming Basics
  • Creating your own object type requires a bit of creativity. You get to made some design decisions regarding: data attributes and procedures.
  • Creating a Python class means you get to make these design decisions. For ex. someone created a class for the list object, or the dict object.
  • Using a class that you (or someone else wrote) involves creating objects of that new type. For ex. we've been creating many lists to manipulate: L1 = [3,5,1] and L2 = [0,0,0] and L1.sort() and L2.extend(L1).
  • Data attributes are variables that describe your object.
  • Data attributes are declared inside a class with self. For ex. self.name = None.
  • Procedures are functions that work with your object. They are called methods.
  • Methods are defined as regular functions whose first parameter is always self.
  • Dot notation is how we call methods on objects. For ex. L.sort() where L is an object of type list and sort is the method.
Methods vs. Functions
  • Methods are always defined inside a class definition.
  • Methods are just functions defined inside a class definition.
  • Methods have self as the first parameter.
  • Methods only work with objects of that class type.
  • Methods use dot notation. For ex. we've seen L.append(5)
  • If you define a function outside of a class, it's just a regular function that we've been creating so far.
  • A regular function defined outside of a class cannot be run on an object using dot notation.

QUICK-CHECK: A post-video exercise to ensure we're all ready for the in-class session!

Which of the following are true? Check all that apply.
There is only one right way to create a Python class to meet a set of specifications.
Defining a Python class automatically creates an object instance of that type in memory.
A Python class definition must always define both data attributes AND methods.
Suppose a method named meth(self) exists. Any object whose class definition contains a method named meth may use dot notation to call meth() on itself.


You've Been Secretly Using Classes

Try: Type these into the repl

LINE1  L1 = list()
LINE2  L1
LINE3  L2 = list((4,5,6))
LINE4  L2

Which of these are true?
LINE1 creates a new data type, called list.
LINE2 accesses an object of type list.
LINE3 creates a new object of type list.
LINE4 accesses an instance of type list.
LINE1  D1 = dict()
LINE2  D2 = dict(((1,2),(3,4)))
LINE3  D3 = dict([[5,6,7],[8,9]])
LINE4  D4 = dict([[5,(6,7)],[8,9]])

Which of these are true?
LINE1 defines a data type, called dict.
LINE2 is equivalent to D2 = {1:2, 3:4}.
LINE3 throws an exception.
LINE4 is equivalent to D4 = {5:(6,7), 8:9}.

Notes:

  • A list or a dict is a standard Python object so Python lets you create them using shorthand notations involving [] or {}.
  • Underneath it all, they are object types defined using a Python class!
  • Python lets you create a list based on a tuple object.
  • Python lets you create a dict based on some combination of lists or tuple objects. But be careful -- dictionaries map ONE key object to ONE value object.

Mechanics of Defining a Class

Try: Fix each of these code snippets

  • class A(object):
        def __init__(self, a):
            a = a
        def get_a(self):
            return a
    
    mya = A(5)
    mya.get_a()
    
  • class B(object):
        def __init__(self):
            self.b = None
        def get_b():
            return self.b
    
    myb = B()
    myb.get_b()
  • class C(object):
        def __init__(self, L):
            self.myL = L
        def getL(self):
            return self.L[:]
    
    C([1,2,3]).getL()
    getL([4,5,6])

Making Design Decisions

Try: Think about some other attributes of a book: data attributes and behaviors

We will represent a Book object. It's your turn to make some design decisions. Start with this code add more attributes and methods to customize our book object. The test cases here don't test anything beyond what is shown, so don't use them to test correctness of your code.

  1. Work in your IDE to make the design decisions for the Book class definition.
  2. Create a bunch of Book objects and run your methods on them to test your code.
  3. Paste your final customized Book class implementation here.

Be creative and play around with adding all sorts of new data attributes and methods you may want a book to have!

class Book(object):
    def __init__(self, title, author, pages):
        self.title = title
        self.author = author
        self.pages = pages
    def display_info(self):
        return f"{self.title} by {self.author} has {self.pages} pages."
    
book1 = Book("We Never Go Out Of Style", 'L. Crew', 100)        
book2 = Book("I Knew You Were Trouble", 'A. Half', 100)


Try: Implement this class according to this specification.

  • There are three ways you can represent a rectangle.
    • Using two numbers: one or the width and one for the height
    • Using one tuple: the first element represents the width and the second represents the height
    • Using 4 tuples representing the 4 corners of a rectangle:
      • (x_topleft, y_topleft)
      • (x_topright, y_topright)
      • (x_botleft, y_botleft)
      • (x_botright, y_botright)
  • After deciding on this initial representation, write the methods assuming your chosen data attributes (i.e. assuming that representation). Try implementing all 3 of these representations for practice.
  • No matter what representation you choose, creating a Rectangle object and calling a method on it will be the same code (see example test cases below).
  • The implementation should be irrelevant to an outsider just interested in using your Rectangle class!

class Rectangle(object):
    """ A class to represent a rectangle. """

    def __init__(self, width, height):
        """ width and height are ints """
        # your code here

    def compute_area(self):
        """ Returns the area of self """
        # your code here
    
    def compute_perimeter(self):
        """ Return the perimeter of self """
        # your code here
    
    def is_square(self):
        """ Returns True if the width equals the height, and False otherwise """
        # your code here

# Examples:
my_rectangle = Rectangle(10,5)
print(my_rectangle.compute_area())      # prints 50
print(my_rectangle.compute_perimeter()) # prints 30
print(my_rectangle.is_square())         # prints False

Notes: 3 Possible Solutions

  • Solution 1: Solution that defines the object using 2 attributes: width and height.
    • A Rectangle object is created with Rectangle(10,5).
    • class Rectangle(object):
          def __init__(self, width, height):
              self.width = width
              self.height = height
          def compute_area(self):
              return self.width * self.height
          def compute_perimeter(self):
              return 2*(self.width) + 2*(self.height)
          def is_square(self):
              return True if self.width==self.height else False
  • Solution 2: Solution uses only one data attribute.
    • Data attribute is a tuple with two components.
    • A Rectangle object is created the same way as Solution 1: Rectangle(10,5).
    • class Rectangle(object):
          def __init__(self, width, height):
              self.dimensions = (width, height)
          def compute_area(self):
              return self.dimensions[0] * self.dimensions[1]
          def compute_perimeter(self):
              return 2*(self.dimensions[0]) + 2*(self.dimensions[1])
          def is_square(self):
              return True if self.dimensions[0]==self.dimensions[1] else False
      
  • Solution 3: Alternate design decision with a different __init__ and implementation.
    • The way you create an object with this implementation is
      Rectangle((1,6), (11,6), (1,1), (11,1))
      not
      Rectangle(10,5)
    • This implementation reads in the 4 corners instead of a value for the width and a value for the height. This implementation looks wordy. And if all we want to do is to get areas and perimeters, then it is!
    • This implementation's strength lies in the fact that it allows flexibility for our Rectangle to have a position! The other implementations above do not, at least as defined right now.
      class Rectangle(object):
          """ A class to represent a rectangle. """
      
          def __init__(self, topleft, topright, botleft, botright):
              """ topleft, topright, botleft, botright are tuples with 2 elements, 
                  representing the 4 corners of a rectangle """
              assert topleft[1] == topright[1]
              assert topleft[0] == botleft[0]
              assert botleft[1] == botright[1]
              assert topright[0] == botright[0]
              self.topleft = topleft
              self.topright = topright
              self.botleft = botleft
              self.botright = botright
      
          def get_width_and_height(self):
              """ Retruns a tuple with the first element representing
              the width and the second elements representing the height """
              width = self.topright[0] - self.topleft[0]
              height = self.topright[1] - self.botleft[1]
              return (width, height)        
      
          def compute_area(self):
              """ Returns the area of self """
              return self.get_width_and_height()[0] * self.get_width_and_height()[1]
          
          def compute_perimeter(self):
              """ Return the perimeter of self """
              return 2*self.get_width_and_height()[0] + 2*self.get_width_and_height()[1]
      
          def is_square(self):
              """ Returns True if the width equals the height, and False otherwise """
              return True if self.get_width_and_height()[0]==self.get_width_and_height()[1] else False
      
      my_rectangle = Rectangle((1,6), (11,6), (1,1), (11,1))
      print(my_rectangle.compute_area())      # prints 50
      print(my_rectangle.compute_perimeter()) # prints 30
      print(my_rectangle.is_square())         # prints False



Please use this box to comment on the content of this lecture (participation!). Post lingering questions, an aha moment, something confusing, interesting, funny, surprising, etc.