DS2000 (Spring 2019, NCH) :: Lecture 8a

0. Administrivia

  1. Due Friday @ 9pm: HW7 (submit via Blackboard)

1. What is a Object-Oriented Programming (OOP)?

So far there has been a strict separation between the "state" of a program (the variables/data that you keep track of in variables) and the code/functions that process that state. You'll hear this referred to as procedural or imperative programming.

Another type of programming, object oriented programming (OOP), takes advantage of situations in which state and code can be logically grouped together into objects. Like the real world, there are types of objects (classes), each of which share properties/fields and operations/methods that can be performed.

OOP is most common when designing relatively large pieces of software. So while you may choose not to employ it in your own programs, you will likely have to interact with object orientation in Python modules (and, in fact, you already have!).

1a. Examples

A string is a class. Once you have an instance of the class (termed an object), you can call methods of the object that refer to its state (the letters that make it up).

In [2]:
x = "hello"
print(type(x))
<class 'str'>
In [3]:
# returns a new object whose state is
# all the letters of x, but upper case!
print(x.upper())
HELLO

A list is a class.

In [4]:
x = [8, 6, 7, 5, 3, 0, 9]
print(type(x))
<class 'list'>

There are functions that take lists as parameters...

In [5]:
print(len(x))
print(sorted(x))
7
[0, 3, 5, 6, 7, 8, 9]

But there are also methods of each list that use/modify its state (the elements in the list)...

In [6]:
x.sort()
print(x)
[0, 3, 5, 6, 7, 8, 9]
In [7]:
x.append(100)
print(x)
[0, 3, 5, 6, 7, 8, 9, 100]

2. Making a Class

A class can be thought of as a template - you are saying all instances of that class (objects) share a set of properties and can perform certain methods.

Let's create a Student class...

  • First question: what does every student have?
    • name, id, list of course grades
  • Second question: what can each point do?
    • report their name & id
    • add a grade
    • compute their GPA
In [8]:
class Student:
    
    # constructor: sets up the initial state
    # of all object fields
    # called automatically when Student()
    def __init__(this, name, id):
        # this refers to the object of
        # type Student that we are creating
        this.name = name
        this.id = id
        this.grades = []
    
    def my_name_id(this):
        # this refers to the object of type
        # Student that came right before .my_name_id()
        return "{} ({})".format(this.name, this.id)
    
    def add_grade(this, grade):
        this.grades.append(grade)
    
    def gpa(this):
        if len(this.grades) == 0:
            return None
        
        grademap = {
            'A':4,
            'B':3,
            'C':2,
            'D':1,
            'F':0,
        }
        
        return sum([grademap[grade] for grade in this.grades])/len(this.grades)
In [9]:
# create a new object
alice = Student("alice", "12345")

# add grades (changes the object)
alice.add_grade('A')
alice.add_grade('B')

# use methods
print(alice.my_name_id())
print(alice.gpa())
alice (12345)
3.5
In [10]:
# create another object
bob = Student("bob", "98765")

# add some grades
bob.add_grade('C')
bob.add_grade('C')

# use methods
print(bob.my_name_id())
print(bob.gpa())
bob (98765)
2.0

3. Special Methods

To make common operations easier, Python supports "magic" methods that start/end with __. There are many, but a couple allow you to perform very common operations:

  • Compare to see whether two objects are the "same" (per what makes sense)
  • Return a string representation of the object

3a. Customized Sameness

By default, == returns true ONLY if both are actually the same object...

In [11]:
class Foo:
    
    def __init__(this):
        this.x = 8675309

x = Foo()
y = Foo()
z = x

print(x == y)
print(x == z)
False
True

However, the __eq__ method allows you to define what "same" means...

In [12]:
class Dog:
    
    def __init__(this, name, tag):
        this.name = name
        this.tag = tag
        
    def __eq__(this, other):
        return isinstance(other, Dog) and other.tag == this.tag

a = Dog('spot', '12345')
b = Dog('spot', '98765')
c = Dog('rover', '12345')

print(a == b)
print(a == c)
print(b == c)
False
True
False

3b. Customized String Representation

By default, str(object) isn't very useful...

In [13]:
print(str(a))
<__main__.Dog object at 0x10e2bc198>

However, the __str__ method allows you to define what gets returned...

In [14]:
class Point2D:
    
    def __init__(this, x, y):
        this.x = x
        this.y = y
        
    def __str__(this):
        return "({}, {})".format(this.x, this.y)

origin = Point2D(0, 0)
print(str(origin))
(0, 0)

4. Class Roster

Let's create a course roster from a file of students

In [15]:
class Roster:
    
    def __init__(this):
        this.students = []
        
    def add_student(this, name, id, grades):
        s = Student(name, id)
        for grade in grades:
            s.add_grade(grade)
        
        this.students.append(s)
    
    def __str__(this):
        return "\n".join([student.my_name_id() for student in this.students])
    
    def avg_gpa(this):
        if len(this.students) == 0:
            return None
        
        return sum([student.gpa() for student in this.students]) / len(this.students)
    
def read_roster(file_path):
    r = Roster()
    
    with open(file_path, 'r') as f:
        for line in f:
            values = line.split()
            r.add_student(values[0], values[1], values[2:])
    
    return r

myroster = read_roster('students.txt')
In [16]:
print(myroster)
alice (12345)
bob (98765)
In [17]:
print(myroster.avg_gpa())
2.75