Lecture 10: Object-Oriented Programming

By the end of this lecture, you will be able to

  • describe scenarios in which object-oriented programing is useful,
  • define classes using attributes and methods,
  • create objects from a class definition.

In this lecture, we will cover the last bit of Python syntax in the course - the rest of the course will consist of more practice and programming techniques. 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.

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. I intentially made some words in bold-faced blue and some words in bold-faced red. Do you see the difference?

Actually, 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:

Okay, well maybe we can keep a list of strings for the first name, a list of strings for the last name, a list of strings for the username, a list of integers for the phone number, a list of MiddImage's for the avatar, and a list of integers for the score. Yikes - that's a lot of lists. Maybe we can use some dictionaries, but that doesn't get us much further. Wouldn't it be nice if we just had some kind of a data structure that represents a contact with all the aforementioned information? First, 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. 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. 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.


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 doesn't make sense to call some "draw" function for a "contact."

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. 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.


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.


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. Next, you tell Python that you are creating a new block of code with a semicolon (:).

Just like with functions, you should place a docstring immediately after your class declaration (the next line after the semicolon). 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) define is the __init__ function. This is a special name for 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). 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 the attributes in the __init__ function, so that they are always defined (in case we try to access them somewhere else). Other programming languages force you to declare all the attributes of a class.

But where am I saving these attributes you ask??? Ah, the self variable (do you see the self variable 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 modify an object should take in (as an argument) this self variable. This self variable should always be the first argument listed in the function definition. This can be confusing, and will make more sense when we talk about accessing attributes and calling methods below.


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 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()). In fact, we are creating 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(). When you do so, you are actually calling the __init__ function defined for that class. You can pass additional arguments into your call to obj = ClassName(), depending on whether your constructor requires input arguments. Note that the order these come in is the same order as they are listed after the self.


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 since these are functions). When Python sees this, it implicitly passes in obj as the self variable as the first argument in the function definition. I know - this is pretty confusing, and will hopefully make more sense once we practice with these concepts.


accessing attributes and calling methods from inside a CLASS definition

What if we want to access attribute1 from inside of 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, calling method1 from method2() would look like self.method1(). And calling method2 from method1() would look like self.method2().


everything in Python is an object

It's time for me to come clean again! 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 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 I kept using the word "object" 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, but don't worry about this part).

Another time we saw objects was actually with the turtle module! In fact, we were using an instance of the Turtle class. This is useful if we want to have multiple turtles - we could create several instances of the Turtle class. Another useful class included in the turtle module is the Screen class. For more information, type help(turtle) after importing the module.


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 an instance of my class?
  2. What functions (methods) would I like to use for 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 __repr__ function 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.__repr__() ). Functions 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 the __str__ function so that Python will print a string representation of your object when you pass your object to the print function.


In the last example, we used the special __repr__ function to print out some information about Point objects. Let's now add a method to draw a point using the turtle module. We will draw a particular Point object as a dot (using turtle.dot), and keep track of two additional attributes in the Point class: the size of the dot, and the color of the dot. We will also modify the to_string method since we don't want to print the 'Hi I'm a Point with coordinates ... part. We will use this to_string method to optionally write the coordinates at the location of the dot - this is useful when debugging graphics programs!


Let's do another geometric application (I like geometric applications 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?


© Philip Claude Caplan, 2021