Object-Oriented Programming

0. Learning objectives

  • Describe scenarios in which object-oriented programing is useful,
  • Define classes using attributes and methods,
  • Create objects from a class definition.

So far, we've seen two types of programming. The first one, functional programming, involved writing functions in which we achieved repetition using recursion: this was function-centric. The second type, imperative programming, involved using data structures such as lists and dictionaries to process larger chunks of data using loops (for- and while-loops): this was data-centric. What if we need both?

Well, object-oriented programming gives us both. We will be able to define some new custom types that

  1. contain some data and
  2. have functions defined for our custom types



Everything in Python is an object.
This means the types we have seen so far (integers, floats, strings, lists, dictionaries, tuples) are all objects.

1. A first attempt: writing our own Snapchat

Suppose we want to write our own Snapchat application: let's call it MiddSnap. Before doing so, let's brainstorm some basic functionalities we want our application to provide:

  • send a snap to a list of contacts,
  • receive a snap from a contact,
  • send a text message to a contact,
  • receive a text message from a contact,
  • modify the image associated with a snap,
  • add text to a snap,
  • sketch a drawing on a snap,
  • add a new contact to a list of contacts,
  • add a snap to a list of memories.

Of course, there are many more features we may like our MiddSnap application to have, but let's just stick to the above list for now. Some words are in bold-faced blue and some words are in bold-faced red. Can you tell the difference? The bold-faced blue words are functions we would like our application to achieve, whereas the bold-faced red words are data we would like to store in our application. Of course, the functions operate on some of these pieces of data, so the two are related. Let's focus on the data for now. You might notice the word "contact" appears several times, for example, we store a "list of contacts", or we send/receive something to/from a contact. How should we represent a contact? Specifically, what data do you need for a contact? How about:

We could keep a list of strings for the first names, a list of strings for the last names, a list of strings for the usernames, a list of integers for the phone numbers, a list of MiddImage images for the avatars, and a list of integers for the score. That's a lot of lists! Also, we need to somehow make sure corresponding items are stored at corresponding indexes to make access easier. On top of that we would need to make sure that everything stays synchronized as we add or remove contacts... That's a lot of bookkeeping to do! Maybe we can use some dictionaries, but that doesn't really get us much further.

Wouldn't it be nice if we just had some kind of "data structure" that represents a contact with all the aforementioned information?

Let's make things a bit worse! What if we want to store an actual Snap (maybe in our list of memories)? The most obvious thing we should store for a Snap is an image. So maybe we can just store a list of images as our memories (remember that you can make lists of whatever you want!). But wait - to reconstruct the memory, some snaps have text, some don't, and some might have some drawings. So we should probably store an extra list of booleans like has_text or has_drawing to fully reconstruct the Snap whenever we want to see our memory again. And then we can store the actual text as a list of strings and the drawing as another list of images. Since some snaps may or may not have some text or drawing, this adds a bit of complication to the data we need to keep track of... This would require quite some planning to put together with the current tools in our belt...

Again, it would be really nice if we could just store some all-encompassing piece of data for a snap that contains the image, text, drawing, etc.


2. Classes & objects

Luckily, we can address all of the concerns we had when developing MiddSnap by using object-oriented programming. This is a technique in which we compartmentalize (i.e abstract) the data we have into "objects." For example, we would create an object to represent a "contact" that would store the first name, last name, username, phone number, etc. of a person in our list of friends. Furthermore, we could create an object to represent a "snap" that would store an image, some banner text and a drawing we would like to send and/or store as a memory.

Next we would define some functions that are designed specifically for each object. For example, we would define a draw function for a snap. This function would be specific to a "snap" object - it wouldn't make sense to call a "draw" function for a "contact."

Each object would be a nice package that contains some data and the functionality that goes with that data.

In Python, we can create objects by defining classes. Think of a class as the blueprint for a house - it's the thing that allows you to make several houses from the same blueprint! Class definitions in Python follow the same idea. We define a class, from which we can create an object (or several objects) of that class. An object is also called an instance of a class. As a matter of fact, we often say that we "instantiate" an object rather than "create".

Please repeat this to yourself several times as it is often a source of confusion: an object is an instance of a class.


A class definition is like the blueprint for a house.



Any house is a constructed instance of the blueprint, maybe with a different wall color or door color.


2.1. Attributes & methods

Before we jump into some Python syntax for defining classes and creating objects from our class definitions, let's revisit the idea of data & functions we wanted for our MiddSnap application. The data that is stored in a class is called the attributes (or properties). The functions that are defined in a class definition (which can be used by objects) are called methods.


2.2. Defining a class

To define a class, we need to tell Python that we are declaring a class. This is done with the class keyword. Python will then expect a new block of code (i.e. it is indented as usual) following this class declaration. After our class declaration, we need to specify the name of the class. It is common to use CamelCase (not drinkingCamelCase) to name your classes (also known as CapWords convention). Next, you tell Python that you are creating a new block of code with a colon (:).

Just like with functions, you should place a docstring immediately after your class declaration (the next line after the colon). Thus, typing help(ClassName) will print this docstring as well as any methods you define in your class definition.

Okay, now we're ready to define some methods. The first method you will always (always, Always, ALWAYS, ALWAYS) define is the __init__() method (double underscore before and double underscore after!). This is a special name for what is commonly known in object oriented languages as the constructor. In keeping with our house analogy, the __init__() method (or constructor) is like the construction company that is building your house (an object) from the blueprint (a class definition). The job of the __init__() method is to initialize any attributes defined for the class (even if they are empty or undefined when an object is created). Doing this creates what is know as a valid object, one where all attributes/properties are specified in the __init__() method. Aside: in Python, you don't actually need to initialize all the attributes (you might want to add some later on), but we are going to make this a convention to always initialize all the attributes for our class in the __init__() method, so that they are always defined (in case we try to access them somewhere else). Other programming languages will force you to declare all the attributes of a class in the constructor so, let's get familiar with this practice.

But where am I saving these attributes you ask??? Ah, the self parameter (do you see the self parameter as the first argument to the __init__() method?). This is a reference to the object we are building. Any method you define in a class definition that you want to use to interact with an object should always have as its first parameter self. Once again, the self variable should always be the first parameter listed in the method definition. This can be confusing, and will make more sense when we talk about accessing attributes and calling methods below.


2.3. Creating an object

Remember when you type x = 2, this really means "create an integer and store it in a variable called x". Well, the truth is that, in Python, x is an object of class int (recall the output of type(x)). This means that somewhere deep down in Python there is a class definition for an integer. In fact, all the types we have seen so far are objects: floats, strings, lists, dictionaries, etc. Everything in Python is an object!

Recall the methods we used to create an empty list (L = list()) or an empty dictionary (D = dict()). What these do, behind the scene, is to create objects that are instances of a list or dict class.

The same is true for our own custom classes - we create objects by calling obj = ClassName() Yes, we yse the name of the class followed by parentheses, as if it where a function because it is!. When you do so, you are actually calling the __init__() method defined for that class. If your __init__() method takes additional parameters, you can pass them in your call to obj = ClassName(). Note that the self parameter is not included and the order these come in is the same order as they are listed after the self.


2.4. Accessing attributes and calling methods using an OBJECT

Now, suppose that you have created an instance of your class ClassName, using obj = ClassName(). In order to access the attributes of the class (those defined in the __init__() method), we will use dot notation. For the example above, we could access obj.attribute1 and obj.attribute2.

In order to call a method of a class (those defined within the block of code in the class definition), we also use dot notation. In the same example, we would call obj.method1() or obj.method2() (remember to use parentheses for methods since they are functions). When Python sees this syntax, it implicitly passes obj as the self parameter, the first argument in the methods definition. So you don't have to pass anything to self.


2.5. Accessing attributes and calling methods from inside a CLASS definition

What if we want to access attribute1 from within method1? Well, since we passed in a reference to the object (the first argument, i.e. self, to method1), then we can access attribute1 of self directly! Remember, self is an object (an instance of the class) too, so we can access attributes and methods in the same way as the previous section. Thus, using attribute1 within any method would look like self.attribute1, calling method1 from method2() would look like self.method1(), and calling method2 from method1() would look like self.method2().


2.6. Everything in Python is an object

In reality, you have been using objects all along! Remember when we typed type(2) or type(1.5) and saw <class 'int'> and <class 'float'> in return? This is because, as we mentioned before, numbers, lists, dictionaries, strings, etc. are all objects. You can use the dir function to list out all the methods for one of these built-in types.

Remember how we kept using the word "object" and "method" when talking about MiddImage? It's because they're objects too! If you open up middimage.py you'll see the class definition for MiddImage. A MiddImage object has attributes for the width, height, channels and the pixels (stored in an attribute called _data - which is a valid variable name).

Another time we used objects, was actually with the turtle module! In fact, we were using an instance of the Turtle class. We didn't mention this but, if we want to have multiple turtles, we could create several instances (objects) of the Turtle class.


Examples

Example 0: points

Before building more advanced applications with object-oriented programming, let's study some smaller applications. In this example, we will write a class to represent a point in $2d$. We will break up this example into a few parts. In this first part, we want to write a Point class that simply represents a point in the plane with $x$ and $y$ coordinates. In part II (the next section), we will add some attributes and methods to visualize these points as dots.

When defining a class, you should ask yourself a few questions:

  1. What data (attributes) would I like to store in each instance (object derived from) of my class?
  2. What functions (methods) would I like to have available to interact with instances of my class?

In this first part, we'll keep things simple. We will just store the $x$ and $y$ coordinates of the point. We will also provide methods to calculate

  1. the distance between the point and the origin, and
  2. the distance from one point to another
We will also provide a method to print out the coordinates of the point in a nice way.

Note that we used the __str__ method to print some information about a Point object. This is the function that is called when you type print(pt), where pt is an instance of the Point class. In fact, it's equivalent to calling print( pt.__str__() ). method with two underscores (__) on either side of the function name are special functions that Python looks for when you try to

  1. use other built-in functions on your custom objects (such as print, str or len) or
  2. perform operations on your custom objects. Yes, this means you can define + for your custom objects!
You can get a sense of what kinds of special functions you can write for your custom types by typing dir(2) or dir([]). You can also overload (redefine) the __repr__ function so that Python will show a representation of your object when you type it in the console or shell

Example 2: triangle soup

Let's do another geometric application (geometric applications are cool because they really help you visualize the concepts). This time, we will write a Triangle class which keeps track of the bottom left corner of the triangle $(x, y)$ and the side length $L$. We will also store a "tilt angle" which is the angle the bottom side of the triangle makes with the x-axis. A color can also be provided (defaults to 'blue' if no color provided) that will be used when drawing a triangle object with the turtle. Thus we will write a draw method for the Triangle class.

It may also be useful to write area and perimeter methods, but we'll skip this for now. What other methods do you think would be useful to define for the Triangle class?

Example 3: bouncing balls

This is a great example combining a lot of the elements we have seen so far in something that uses a typical animation technique!