CS21 Week 13: Object-Oriented Programing

Week 13 Topics

  • Object-Oriented Programming

  • Defining Classes

  • The implicit self parameter

  • Class methods:

    • The constructor, __init__

    • String conversion, __str__

    • Access methods (getters)

    • Mutator methods (setters)

Monday

Finishing Recursion

Before moving on to new content, let’s wrap up recursion.

Objects Overview

So far, we have used lots of objects and their methods. Recall that an object consists of both data and methods — an object knows stuff and can do stuff. Here are some examples of objects we have used:

  • string objects (type str):

    • data: characters in the string

    • methods: .split(), .strip(), .isdigit(), etc.

  • list objects (type list):

    • data: elements in the list

    • methods: .append(), .sort(), etc.

  • Zelle Graphics objects (e.g. Circle):

    • data: radius, center point, fill color, etc.

    • methods: .draw(), .move(), .setFill(), .getCenter(), etc.

  • HydroDam objects from labs 9 & 10

Why Objects?

Consider the student records CSV file we looked at during weeks 9 and 10; the first time through, we just stored this file as a list of strings (one per line), and then split each line into another list of strings (one per field).

Then later we used a class called StudentRecord to store each line instead of just a list of strings. This made our code easier to read, write, and understand; compare the following:

  • grade = students[i].get_GPA()

  • grade = float(lines[i].split(",")[3])

Even if both lines accomplish the same thing, it’s a lot more clear what’s going on in the first one. Our goal in using classes is to make development easier by increasing modularity and abstraction, so we don’t have to remember every detail (such as which index in a list of strings contains the GPA) at all times.

Classes and Object-Oriented Programming

In object-oriented programming, we saw that objects described complex types that contained data and methods that could act upon the data. For example, we saw that lists and strings were objects, as were elements of the Graphics library. Each of the objects we created stored data (e.g., a center, radius and color for Circle objects) and methods (e.g., move(), getCenter(), setFill()).

Encapsulation

The idea of encapsulation is to take a bunch of related data and functionality, and package ("encapsulate") it together in a single object. This has several advantages:

  • can pass a single argument to a function, instead of having to pass a whole bunch of related parameters

  • once it’s written and works, we don’t need to worry about the details of how (e.g. we don’t know what the inside of the list class looks like, and that’s ok because we don’t need to)

Defining a Class

Creating a class defines a new type of object. Classes define the specific methods and data an object stores. To start, let’s look at the definition of the StudentRecord class we’ve been using.

Open up student.py. What do you think each line is doing? What can you explain with your neighbor?

The first key element is the class definition, which follows the format:

class <ClassName>(object):

<ClassName> is a placeholder for the actual name you want to give; class is a keyword to indicate that you are defining a new class. The additional item, (object), is an indication that the class will follow certain protocols (note that object is what you’ll type, this one is not a placeholder). You should assume that this will always be there (you can put something else in there if you want to build upon an existing class, but that’s beyond our CS 21 discussion).

Methods

Within the class, you will see several class methods. Methods follow the same format as functions, but have the distinction of belonging to a specific class. As always, Python uses indentation to define what belongs, so we every definition that’s part of a class must be indented in the class definition block.

class ClassName(object):
    def __init__(self, param1, ...):
        #Constructor - initializes object and data

    def getter1(self):
        #Return some piece of information

    def setter1(self, valToSet):
        #Change something about the data

General methods

Normal methods can be named and defined like any other function, but must have a special parameter called self, which must always be the first parameter. self is a reference to the object the method is being called on, and is used in a method to refer to data in the object.

  • self.name: a "name" field that’s part of the object referred to by self; this is called an instance or member variable

  • name: this is just a regular variable that will behave like it would in any other function

The key distinction is that member variables are part of the object, so they last as long as the object does; this is different from normal "local" variables that go away when the function or method returns.

Special methods

Python has some special methods that use double underscores around their names; these get called automatically in some circumstances. There are a number of them, but the two most useful ones for us will be:

  • __init__: the constructor, called automatically when an object is created

  • __str__: the to-string method, called automatically when an object is passed to print()

Constructor

Python will run the __init__(self, x, y) method of a class every time we create an object of that type. Every class must have a constructor — it specifies the initial data we need to keep track of and gives it a starting value. There are many other special methods that we’ll skip for right now. These include __len__ (to obtain the length of a sequence e.g., len(ls) calls the list.__len__() method) and __str__ (to convert an object to a string representation, str(x)).

Wednesday

self parameter

self is perhaps the single most confusing feature of python classes. You will see self everywhere when you are writing and designing a class. However, you will not see self at all when using a class, which is why we haven’t talked about it until this week.

Notice that all methods in a class have a self parameter as the first parameter. This is required, but users of the class do not provide this argument; instead Python automatically assigns the object that called the method as the self parameter. For example, if we create a StudentRecord object, student and call student.get_gpa(), the self parameter points to student. This is common source of confusion when writing your first class. If you look at the definition of get_gpa(self), it looks like you need to pass in one argument for the self parameter. But since Python does this automatically, users actually call this method with zero arguments, e.g., student.get_gpa(). Calling get_gpa with an extra argument will result in a seemingly bizarre error message:

student.get_gpa(1)
TypeError: get_gpa() takes exactly 1 argument (2 given)

Note that providing one user argument to get_gpa() reports an error that two arguments were provided. The automatic self was the other (and technically first) argument.

In summary, always put self as the first parameter of a class method when writing a method, but ignore this parameter when calling the method.

The meaning of self

You may be wondering: if self is part of every method, but it’s ignored by the user, why do we need it in the class definition? Recall that objects both know stuff and do stuff. The method definitions inside the class are how classes/objects do stuff, and the self variable is how a particular object knows stuff. In any method, we can declare a variable with a self. prefix, (e.g., self.course = "CS21"). If we set the value of one of these special self. attributes in one method, we can access the value in another method without passing an additional parameter. In effect, all methods share all the self. attributes.

Typically we use the __init__ method to define and initialize values for all attributes we will be using at some point in the class.

Testing a Class

Just as with top-down design, you should practice incremental development when defining a class. Usually, this involves writing a function, thoroughly testing it, moving onto the next function, etc. There are several options for how to test, including using a separate main program. However, a common strategy is to include some test code at the bottom of your class definition file. This way, the test code can always be run in the future when updates are made. To do so, remember to include the following lines at the bottom of your file:

if __name__ == "__main__":
    # Write testing code here

The first line ensures that the testing code does not run when you import the file, but rather only when someone calls the file from the command line:

$ python3 student.py

Exercises: Modify Student class and test

  • Let’s modify the StudentRecord class to add some getter and setter methods;

  • be sure to test your program at each stage, and don’t move on until you’re confident it works correctly.

    1. add a get_age() method to the class

    2. update the print_student() function in readdata-obj.py to use this new method

    3. add a set_gpa() method to the class

    4. add a set_age() method to the class; make sure it does not allow negative values

Friday

Objects, scope, and internal method calls

Reminder: inside a class definition, the object is referred to as self. If we want to access its data, we use dot notation e.g., self.name is that records the student’s name. The name variable’s scope is the object itself rather than the function it was originally defined in.

Similarly, from within a method of a object, you can call another method by referring to the self variable. For example, if a class has two methods, method1(self, …​) and method2(self, …​), you can call method1 from within method2:

class Thing(object):
    def method1(self, x, y, z):
        ...

    def method2(self, ...):
        ...
        self.method1(a, b, c)  # Call method1() on self from within method2
        ...

Why objects?

Objects provide encapsulation. In computer science, encapsulation can mean one of two related things:

  • A mechanism for restricting access to some of the object’s components.

  • A mechanism for bundling of data with methods operating on that data.

Classes and objects provide both mechanisms. On larger programming projects, it is common to have different people work on different parts of the program. Classes are a good place to divide the work. In this case, the class writer and the class user can agree on an interface. The interface specifies what methods the class has and what they should do. The class user doesn’t need to know or care how a class is implemented, only how to use the objects. The class writer doesn’t need to know or care how a class is used, only how to implement the interface.

Object-oriented programming also facilitates modularity. Modular programming is a software design technique that focuses on separating the functionality of a program into independent, interchangable pieces (aka "modules"), so that each piece contains everything needed to perform one aspect of the desired functionality.

Class definitions provide reusability, i.e., they let you create/reuse functionality while hiding technical details. With classes, you can rapidly create new and complex code by grabbing existing code "off the shelf" and reusing it for novel purposes.

Exercises: Modify Student class and test

  • Let’s modify the StudentRecord class to add a list of course grades; be sure to test your program at each stage, and don’t move on until you’re confident it works correctly.

    1. add a line to the constructor that creates an empty list called self.grades

    2. define a method get_grades that returns the list of grades

    3. define a method add_grade that takes a grade as a parameter and appends it to the list of grades

    4. define a method compute_gpa that goes through the list of grades and re-calculates the student’s GPA. This method should change self.gpa so it has the new value; it should also return that new value.