Week 9: Classes and Object-Orientation

Readings

Since we're moving to shorter class sessions, it's really important that you do the readings and exercises before the class session. Every class will start with a prompt to come up with three questions based on the reading material. For complete chapters, please do the end-of-chapter exercises as well.

Session 2

Key Questions

  • What is an object in Python, and how does it differ from a class?
  • When I use dot syntax to call a method on an object, how does Python know what code to execute?
  • How do I define a class with instance variables (state) and methods (behavior)?
  • What is the meaning of self in a method definition's parameter list?
    • How do I write methods on an object that call other methods of the same object?
  • Why might I want to design new "collection" classes when list, dict, and so on exist?

Topics

"Objects"

Objects encapsulate state and behavior; the underlying metaphor is that the program is built up out of tiny computers that communicate with each other along well-defined interfaces, and no object needs to (nor can) know anything about the internal state and behavior of any other. This style of program design is called "object orientation"; for a while it was seen as an unalloyed good, and nowadays it is considered one reasonable approach among several. It's pervasive in Python and Java, so we will explore it this week in 51a.

For example, a list is an object with methods like append and reverse:

>>> l = [1, 2, 3, 4]
>>> l.append(5)
>>> l
[1, 2, 3, 4, 5]
>>> l.reverse()
>>> l
[5, 4, 3, 2, 1]

Methods

In the tiny computers metaphor, methods are behaviors of the computers which are activated by particular messages; for example, when I tell a list like [1,2] to reverse (the message), it executes its underlying reverse code (the method). Broadly, there are two kinds of methods: those that mutate (modify) the receiver, and those that only access it and do not change it. Python makes no syntactic distinction between these two types, but some languages (like Rust) do.

You can look up all the messages an object responds to (i.e., all the methods it implements) using e.g. help(x) (for a variable x) or help(list). dir(x) is like help, but just gives method names.

Constructors

Every object type has a special method called a constructor. It has the same name as the object's type and can be called on its own to build (construct) a new object of that type. Interestingly, many of the methods we've actually been using already are actually constructors!

>>> str(10)
'10'
>>> int("10")
10
>>> float(10)
10.0

These each construct a new object of the corresponding type. Even if a type offers special syntax (like the braces used in building a dict or list), it also has a constructor (in Python's standard library, many of these constructors try to convert the given value into that type).

>>> list()
[]
>>> tuple()
()
>>> list( "abcde" )
['a', 'b', 'c', 'd', 'e']
>>> tuple( "abcde" )
('a', 'b', 'c', 'd', 'e')
>>> tuple( [1, 2, 3, 4] )
(1, 2, 3, 4)

Classes

A class can be thought of as a "blueprint" for creating many similar objects, all with the same methods. Every object in Python is an instance of some class. (Other programming languages may have some values of class or object types and others of base types that are not "object-y", some have objects but not classes, and some programming languages do not have objects at all!)

For example, we could define a class Person:

  • A person has some attributes (name, mailing addresses, birth date)
  • A person has some methods or behaviors (calculating their age from birth date)
  • When we define a particular person, it will be an object which is an instance of class Person.

To define a class we use a new type of block, the class block:

class Person:
    def __init__(self, name): # constructor; must take self, may take more parameters
        self.name = name      # store the name in this object right here, self, the receiver
    # ... other methods

Check out person.py:

  • 5 methods
  • 2 "special" methods
    • Python uses these pervasively
    • They start and end with two underscores
    • __init__ defines the constructor
    • __str__ defines how to stringify this object, i.e. how str(pers) or print(pers) will work.
    • We usually do not call e.g. person.__str__(), but rely on Python to magically call the right special methods at the right times.
  • self
    • self is a variable which is the implicit first parameter of all methods denoting the receiver
      • When you call a method of an object, the receiver goes to the left of the dot.
    • When we call pers.get_shirt_color(), self (inside the call to get_shirt_color) will be whatever the value of pers is.
    • We can use self to remember state across method calls (e.g. in the constructor or in get_shirt_color) using dot syntax
  • Look closely at the constructor for Person:
    • Takes self and two parameters
    • Saves these parameters into the receiver (i.e. self)
  • The methods beginning with get_ are clearly meant to be accessors, so it would be surprising if they modified the receiver. What parameters do they take?
    • On the other hand, check out change_shirt; is it a mutator or an accessor?
  • Try defining a class called Rectangle; what parameters should its constructor take, and what would be good names for its instance variables?

Modeling Systems with Objects

Rectangle

Let's do some computational geometry. This is a really interesting field, with lots of applications to everything from simulations to game programming to robotics to graphics and beyond. We'll start with one of the most widely-used shapes of all: the "axis-aligned bounding box", which in two dimensions is a fancy way of saying "rectangle".

On your own, think about what properties of a rectangle are important; in other words, how many numbers do you need to define a rectangle?

There's two ways we generally talk about rectangles:

  • A top left corner (x1,y1) and bottom right corner (x2,y2)
  • A top left corner (x1,y1) and an extent or size (w, h).

What are some tradeoffs between those choices (hint: is it possible to create "impossible" rectangles)? Decide on one and define the Rectangle class and its constructor. You can refer to Person to see how to define constructors.

Next, try implementing a method def area(self) which returns the area (width times height) of the rectangle.

When you're done, compare with rectangle.py!

We make code for people

The most important thing to remember about code is from Abelson and Sussman's Structure and Interpretation of Computer Programs: "Programs must be written for people to read, and only incidentally for computers to execute." What do you think this means?

It's easy to get bogged down in making your program fast, or to make it work in a very mechanistic way where the flow of control is difficult for people to understand. Objects can help us with that in two main ways: like modules, they serve as a way to organize functionality so it's easy to know where to find it (operations on strings live in the str class); they also hide details of how operations are implemented, so we can send the same message to multiple different types of objects without worrying how they're implemented (or use an object with behavior and state without knowing what variables are packaged up inside of it). You've been using for example lists, strings, and dictionaries in exactly these ways all along!

Encapsulation

  • look at the Rectangle class in rectangle2.py
    • we have the exact same set of methods (constructor and area)
    • however, we have different internal representation
      • we store the lower left hand corner (x,y)
      • and the width and the height
      • notice that we can ask the exact same questions using this representation
    • anyone using the Rectangle class should NOT care which version we use
      • both have the same set of methods and the same functionality
    • this is the power of using classes, a general framework called object-oriented programming
    • Notice that we can do the same operations and it doesn't change the view from outside the class
  • modularity
    • functions create single units that we can use to build up other functions
    • in the same way, classes allow us to create functional units (in this cases a class of objects with a particular behavior)
    • avoid naming conflicts

Data Structure: Queue

Look at the Queue class in queue.py.

  • How many methods does it have and what do they do? How many parameters do you need to instantiate a Queue?

    >>> q = Queue()
    >>> print(q)
    The queue contains: []
    >>> q = Queue([1, 2, 3, 4])
    >>> print(q)
    The queue contains: [1, 2, 3, 4]
    
    • What would this code print out? Why?

      >>> l = [1,2,3]
      >>> q = Queue(l)
      >>> l.append(4)
      >>> print(q)
      
  • Look at the add and remove methods. What is this class for?
    • It's just a way of storing data
      • A "queue" in the sense of a line at the grocery store
      • first things to be added are the first things to be removed
      • sometimes called FIFO (first in first out)
    • add adds elements to the back of the list
    • remove removes elements from the front of the list
    • If we only get elements out using remove, and only add them using add, then we can only use this in that oldest-to-newest order.
  • Example usage:

    >>> q = Queue()
    >>> q.add(1)
    >>> q.add(10)
    >>> q.add(2)
    >>> print(q)
    The queue contains: [1, 10, 2]
    >>> q.remove()
    1
    >>> q.remove()
    10
    >>> q.remove()
    2
    
  • is_empty
    • just checks if the queue has anything in it
  • notice that internally, this is still just a list
    • We get to use Python's well-tested and efficient list collection….
    • But by hiding the list in the class, we have:
      • provided a clear small set of methods that defines how we can interact with the object (the queue)
      • hid the implementation details from whoever uses it
        • we used a list, but could have used something else
        • in a similar way, we could have added to the front of the list and removed from the back and still achieved exactly the same functionality

Data Structure: Stack

Look at the Stack class in stack.py.

  • How many methods does it have and what do they do? How is this similar to and different from Queue?
    • Stacks add and remove things from the same end of the list.
  • In a stack, we tend to use the names push and pop for add and remove.
    • think of it like a stack of plates
      • the last plate that you put on top
      • will be the first one to be removed
      • sometimes called LIFO (last in first out)
  • For example:

    >>> s = Stack()
    >>> s.add(1)
    >>> s.add(10)
    >>> s.add(2)
    >>> s.remove()
    2
    >>> s.remove()
    10
    >>> s.remove()
    1
    

Exercise: Fruit

We're going to design a Fruit class. It will have the following constructor and methods:

def __init__(self, name, color):
    self.name = name
    self.color = color
    self.eaten = False
    self.age = 0
  • is_eaten takes zero arguments and returns a boolean indicating whether or not the fruit is eaten
  • eat takes zero arguments and "eats" the fruit
  • allergy_check takes a color and returns true if the fruits color is the same as the input color, false otherwise
  • age_fruit takes zero arguments and ages the fruit by a day
  • __str__ prints out a string version of the fruit
  • For example, if we ran:

    fruit = Fruit("banana", "yellow")
    print(fruit)
    print(fruit.allergy_check("red"))
    fruit.age_fruit()
    print(fruit)
    print(fruit.is_eaten())
    fruit.eat()
    print(fruit.is_eaten())
    
    • We would get:

      yellow banana that is 0 days old
      False
      yellow banana that is 1 days old
      False
      True
      
  • When you're done, check your version against fruit.py.

Programming by Messaging

  • Look at the Rectangle class in rectangle3.py
    • Another version of the Rectangle class that we saw earlier
    • Like in rectangle2.py, we keep track of the x,y coordinates of the top-left corner and the width and height
  • If we print out the rectangle we see the position of the rectangle and the area:

    >>> r = Rectangle(0, 0, 10, 20)
    >>> print(r)
    >>> Rectangle at (0, 0) with area: 200
    
    • In the __str__ method, we call the area method
  • Anytime you want to call another method from within the class you write self.method_name, e.g. self.area()
    • The equals method takes one parameter as input. What is the parameter?
      • Another rectangle!
      • equals is a method we wrote that takes one parameter, another rectangle, as input
      • in the body of the method then there are two rectangles:
      • self
      • another_rectangle
      • We can access the instance variables of the parameter rectangle (another_rectangle) in the same way we can self.

Author: Joseph C. Osborn

Created: 2020-03-13 Fri 13:41

Validate