Classes#

  • objects

  • class

    • attributes

    • methods

  • instances

    • __init__

Objects#

Objects are an organization of data (called attributes), with associated code to operate on that data (functions defined on the objects, called methods).

Class Question #1#

Given what we’ve discussed in this course so far, if you wanted to store information about a date, how would you do so?

  • A) string

  • B) dictionary

  • C) list

  • D) integers stored in separate variables

Storing Dates (Motivation)#

# A date, stored as a string
date_string = '29/09/1988'
print(date_string)
# A date, stored as a list of numbers
date_list = ['29', '09', '1988']
date_list
# A date, stored as a series of numbers
day = 29
month = 9
year = 1988

print(day)
# A date, stored as a dictionary
date_dictionary = {'day': 29, 'month': 9, 'year': 1988} 
date_dictionary

Objects are ways to organize data (variables) and functions together.

Example Object: Date#

# Import a date object
from datetime import date   
date? 
# Set the data we want to store in our date object
my_day = 29
my_month = 9
my_year = 1988

# Create a date object
my_date = date(my_year, my_month, my_day)
print(my_date)
# Check what type of thing `my_date` is
type(my_date) 

Accessing Attributes & Methods#

Attributes and methods are accessed with a ., followed by the attribute/method name in the object.

Date - Attributes#

Attributes look up & return information about the object.

attributes maintain the object’s state, simply returning information about the object to you

# Get the day attribute
my_date.day
# Get the month attribute
my_date.month
# Get the year attribute
my_date.year

Date - Methods#

Reminder: these are functions that belong to and operate on the date object directly.

methods modify the object’s state

# Method to return what day of the week the date is
my_date.weekday()
# Reminder: check documentation with '?'
date.weekday?

It’s also possible to carry out operations on multiple date objects.

# define a second date
my_date2 = date(1980, 7, 29)
print(my_date, my_date2)
# calculate the difference between times
time_diff = my_date - my_date2
print(time_diff.days,  "days") #in days
print(time_diff.days/365,"years") #in years

Listing Attributes & Methods : dir#

# tab complete to access
# methods and attributes
my_date.

# works to find attributes and methods
# for date type objects generally
date.
## dir ouputs all methods and attributes
## we'll talk about the double underscores next lecture
dir(my_date)

Class Question #2#

Given the code below:

my_date = date(year=1050, month=12, day=12)

Which is the best description:

  • A) my_date is an object, with methods that store data, and attributes that store procedures

  • B) my_date is variable, and can be used with functions

  • C) my_date is an attribute, with methods attached to it

  • D) my_date is a method, and also has attributes

  • E) my_date is an object, with attributes that store data, and methods that store procedures

Class Question #3#

For an object lets with a method do_something, how would you execute that method?

  • A) do_something(lets)

  • B) lets.do_something

  • C) lets.do_something()

  • D) lets.do.something()

  • E) ¯\_(ツ)_/¯

Class Question #4#

For an object lets with an attribute name, how would you return the information stored in name for the object lets?

  • A) name(lets)

  • B) lets.name

  • C) lets.name()

  • D) lets.get.name()

  • E) ¯\_(ツ)_/¯

Objects Summary#

  • Objects allow for data (attributes) and functions (methods) to be organized together

    • methods operate on the object type (modify state)

    • attributes store and return information (data) about the object (maintain state)

  • dir() returns methods & attributes for an object

  • Syntax:

    • obj.method()

    • obj.attribute

  • date and datetime are two types of objects in Python

Classes#

Classes define objects. The class keyword opens a code block for instructions on how to create objects of a particular type.

Think of classes as the blueprint for creating and defining objects and their properties (methods, attributes, etc.). They keep related things together and organized.

Example Class: Dog#

# Define a class with `class`. 
# By convention, class definitions use CapWords (Pascal)
class Dog():
    
    # Class attributes for objects of type Dog
    sound = 'Woof'
    
    # Class methods for objects of type Dog
    def speak(self, n_times=2): 
        return self.sound * n_times

A reminder:

  • attributes maintain the object’s state; they lookup information about an object

  • methods alter the object’s state; they run a function on an object

class notes:

  • classes tend to use CapWords convention (Pascal Case)

    • instead of snake_case (functions and variable names)

  • () after Dog indicate that this is callable

    • like functions, Classes must be executed before they take effect

  • can define attributes & methods within class

  • self is a special parameter for use by an object

    • refers to the thing (object) itself

  • like functions, a new namespace is created within a Class

# Initialize a dog object
george = Dog()
# the object george has 'sound' attribute(s) from Dog()
george.sound
# the pbject george has 'Dog' method(s)
# remember we used `self`
george.speak()

Class Question #5#

Which of the following statements is true about the example we’ve been using?

class Dog():
    
    sound = 'Woof'
    
    def speak(self, n_times=2):
        return self.sound * n_times
  • A) Dog is a Class, sound is an attribute, and speak is a method.

  • B) Dog is a function, sound is an attribute, and speak is a method.

  • C) Dog is a Class, sound is a method, and speak is an attribute.

  • D) Dog is a function, sound is an method, and speak is an attribute.

Using our Dog Objects#

# Initialize a group of dogs
pack_of_dogs = [Dog(), Dog(), Dog(), Dog()]
# take a look at this
pack_of_dogs
# take a look at this
type(pack_of_dogs[0])
for dog in pack_of_dogs:
    print(dog.speak(6))

Instances & self#

An instance is particular instantiation of a class object. self refers to the current instance.
# Initialize a dog object
george = Dog()

From our example above:

  • Dog is the Class we created

  • george was an instance of that class

  • self just refers to whatever the current instance is

Class Question #6#

How many instances of Dog() are created below and how many times does the speak() method execute?

pack_of_dogs = [Dog(), Dog(), Dog(), Dog()]

counter = 1

for doggie in pack_of_dogs:
    if counter <= 2:
        print(doggie.speak())
        counter += 1
    else:
        break
  • A) 2 instances, 2 method executions

  • B) 2 instances, 4 method executions

  • C) 4 instances, 2 method executions

  • D) 4 instances, 4 method executions

  • E) ¯\_(ツ)_/¯

Instance Attributes#

An instance attribute is specific to the instance we’re on. This allows different instances of the same class to be unique (have different values stored in attributes and use those in methods).

# Initialize a group of dogs
pack_of_dogs = [Dog(), Dog(), Dog(), Dog()]

print(pack_of_dogs[0].sound)
print(pack_of_dogs[1].sound)
pack_of_dogs[1].sound = 'meow'
print(pack_of_dogs[0].sound)
print(pack_of_dogs[1].sound)

This creates four different Dog type objects and stores them in a list. But, up until now…every Dog was pretty much the same.

Instance attributes are attributes that we can make be different for each instance of a class. __init__ is a special method used to define instance attributes.

Example Class: Dog Revisited#

  • Two trailing underscores (a dunder, or double underscore) is used to indicate something Python recognizes and knows what to do every time it sees it.

  • Here, we use __init__ to execute the code within it every time you initialize an object.

class Dog():
    
    # Class attributes for Dogs, since they are defined outside the __init__
    sound = 'Woof'
    bad_foods = ['chocolate', 'onions']
    
    
    # Initializer, allows us to specify instance-specific attributes
    # leading and trailing double underscores indicates that this is special to Python
    def __init__(self, name='Fido'):
        self.name = name
        
        #instance attribute for Dogs, since it is defined in the __init__ method
        self.favorite_foods = ['steak']
    
    def speak(self, n_times=2):
        return self.sound * n_times
# Initialize a dog
# what goes in the parentheses is defined in the __init__
gary = Dog(name='Gary') 
another_dog = Dog()
# Check gary's attributes
print(gary.sound)    # This is an class attribute
print(gary.name)     # This is a instance attribute
# Check gary's methods
gary.speak()

Class attributes vs. Instance attributes#

In our Dog class, sound and bad_foods are class attributes because they are defined outside of the __init__ method, and name and favorite_foods are instance attributes because they are defined within the __init__ method. A change to a class attribute is reflected across all instances of that class, and a change to an instancec attribute is only reflected in that instance. Note that in our example class, sound is a class attribute, but it is a string, which is immutable, and therefore any “change” to it is in reality destroying the existing class attribute “sound” and replacing it with an instance attribute of the same name.

# let's create a couple of new Dogs:
sparky = Dog('Sparky')
spot = Dog('Spot')

# we can see that they have the same values in the class variables:
print("Sparky's sound is: ", sparky.sound)
print("Sparky's bad foods are:", sparky.bad_foods)
print("Spot's sound is: ", spot.sound)
print("Spot's bad foods are:", spot.bad_foods)
# now we change Spot's bad food list:
spot.bad_foods.append('avocados')

# and we can see that this change is reflected in both instances, since it was a change to a class attribute:
print("Sparky's bad foods are:", sparky.bad_foods)
print("Spot's bad foods are:", spot.bad_foods)
# now let's look at the instance attributes:
print("Sparky's name is: ", sparky.name)
print("Sparky's favorite foods are:", sparky.favorite_foods)
print("Spot's name is: ", spot.name)
print("Spot's favorite foods are:", spot.favorite_foods)
# when we change an instance attribute, that change is not reflected in other instances:
spot.favorite_foods.append('turkey')

# we can see that this change is reflected in Spot, but not Sparky:
print("Sparky's favorite foods are:", sparky.favorite_foods)
print("Spot's favorite foods are:", spot.favorite_foods)

Class Question #7#

Edit the code we’ve been using for the Class Dog to include information about the breed of the Class Dog in NewDog?

# EDIT CODE HERE
class NewDog():
    
    sound = 'Woof'
    
    def __init__(self, name):
        self.name = name
    
    def speak(self, n_times=2):
        return self.sound * n_times
polly = NewDog('Polly', 'Corgi')
print(polly.name)
print(polly.breed)

polly.breed = 'Dalmatian'
print(polly.breed)
  • A) I did it!

  • B) I think I did it!

  • C) So lost. -_-

Class example: Cat#

# Define a class 'Cat'
class Cat():
    
    sound = "Meow"
    
    def __init__(self, name):
        self.name = name
    
    def speak(self, n_times=2):
        return self.sound * n_times 

Instances Examples#

# Define some instances of our objects
pets = [Cat('Jaspurr'), Dog('Barkley'), 
        Cat('Picatso'), Dog('Ruffius')]
for pet in pets:
    print(pet.name, ' says:')
    print(pet.speak())

Class Question #8#

What will the following code snippet print out?

class MyClass():
    
    def __init__(self, name, email, score):
        self.name = name
        self.email = email
        self.score = score
     
    def check_score(self):        
        if self.score <= 65:
            return self.email
        else:
            return None
student = MyClass('Rob', 'rob@python.com', 62)
student.check_score()
  • A) True

  • B) ‘Rob’

  • C) False

  • D) ‘rob@python.com’

  • E) None

Code Style: Classes#

  • CapWords for class names

  • one blank line between methods/functions

Good Code Style

class MyClass():
    
    def __init__(self, name, email, score):
        self.name = name
        self.email = email
        self.score = score
    
    def check_score(self):        
        if self.score <= 65:
            return self.email
        else:
            return None

Code Style to Avoid

class my_class(): # uses snake case for name
    def __init__(self, name, email, score):
        self.name = name
        self.email = email
        self.score = score   # no blank lines between methods  
    def check_score(self):        
        if self.score <= 65:
            return self.email 
        else:
            return None

Example: ProfCourses()#

Let’s put a lot of these concepts together in a more complicated example…

What if we wanted some object type that would allow us to keep track of the professor’s courses? Well…we’d want this to work for any Professor, so we’ll call it ProfCourses.

We would likely want an object type and then helpful methods that allow us to add a class to the course inventory and to compare between courses.

class ProfCourses():
    
    # create three instance attributes
    def __init__(self, prof):
        self.n_courses = 0
        self.courses = []
        self.prof = prof
morgan_courses = ProfCourses('Morgan')
print(morgan_courses.n_courses)
print(morgan_courses.prof)

add_course() method

class ProfCourses():
    
    def __init__(self, prof):
        self.n_courses = 0
        self.courses = []
        self.prof = prof
    
    # add method that will add courses as a dictionary
    # to our attribute (courses)...which is a list
    def add_course(self, course_name, quarter, n_students):
        
        self.courses.append({'course_name': course_name, 
                             'quarter' : quarter, 
                             'n_students': n_students})
        # increase value store in n_courses
        # by 1 any time a class is added
        self.n_courses += 1
# create morgan_courses
morgan_courses = ProfCourses('Morgan')

# add a class
morgan_courses.add_course('COGS18', 's123', 80)

# see output
print(morgan_courses.courses)
morgan_courses.n_courses

compare() method

class ProfCourses():
    
    def __init__(self, prof):
        self.n_courses = 0
        self.courses = []
        self.prof = prof
    
    def add_course(self, course_name, quarter, n_students):
        
        self.courses.append({'course_name': course_name,
                             'quarter' : quarter,
                             'n_students': n_students})
        self.n_courses += 1
            
    # add method to compare values in courses
    def compare(self, attribute, direction='most'):
    
        fewest = self.courses[0]
        most = self.courses[0] 
        
        for my_course in self.courses:
            if my_course[attribute] <= fewest[attribute]:
                fewest = my_course
            elif my_course[attribute] >= most[attribute]:
                most = my_course
                
        if direction == 'most':
            output = most
        elif direction == 'fewest':
            output = fewest

        return output
# create morgan_courses
morgan_courses = ProfCourses('Morgan')

# add a bunch of classes
morgan_courses.add_course('COGS18', 's123', 80)
morgan_courses.add_course('COGS108', 's223', 80)
morgan_courses.add_course('COGS101a', 'sp23', 160)
morgan_courses.add_course('COGS18', 'fa23', 300)

# see the courses
print(morgan_courses.n_courses)
morgan_courses.courses
# make comparison among all courses
# returns the class with the most students
morgan_courses.compare('n_students')
# return the class with the fewest students
morgan_courses.compare('n_students', 'fewest')

extending the functionality of the compare() method by adding more attributes

class ProfCourses():
    
    def __init__(self, prof):
        self.n_courses = 0
        self.courses = []
        self.prof = prof
    
    def add_course(self, course_name, quarter, 
                   n_students, n_exams, n_assignments):
        
        # add in additional key-value pairs
        self.courses.append({'course_name': course_name,
                             'quarter' : quarter,
                             'n_students': n_students,
                             'n_exams' : n_exams,
                             'n_assignments' : n_assignments})
        self.n_courses += 1
             
    def compare(self, attribute, direction='most'):
    
        fewest = self.courses[0]
        most = self.courses[0] 
        
        for my_course in self.courses:
            if my_course[attribute] <= fewest[attribute]:
                fewest = my_course
            elif my_course[attribute] >= most[attribute]:
                most = my_course
                
        if direction == 'most':
            output = most
        elif direction == 'fewest':
            output = fewest

        return output
# create morgan_courses
morgan_courses = ProfCourses('Morgan')

# add a bunch of classes
morgan_courses.add_course('COGS18', 's123', 80, 2, 5)
morgan_courses.add_course('COGS108', 's223', 80, 2, 5)
morgan_courses.add_course('COGS101a', 'sp23', 160, 3, 0)
morgan_courses.add_course('COGS18', 'fa23', 300, 3, 5)

# see the courses
print(morgan_courses.n_courses)
# return the class with the most exams
morgan_courses.compare('n_exams', 'most')
# return the class with the fewest assignments
morgan_courses.compare('n_assignments', 'fewest')

Improving & updating this code

  • account for ties in compare()

  • edit code in compare() to make the for loop and following conditional more intuitive

  • add a method to put course list in time order

  • etc.

Classes Review#

  • class creates a new class type

    • names tend to use CapWords case

    • can have attributes (including instance attributes) and methods

      • obj.attribute accesses data stored in attribute

      • obj.method() carries out code defined within method

  • instance attributes are defined with __init__

    • __init__ is a reserved method in Python

    • This “binds the attributes with the given arguments”

    • self refers to current instance

  • to create an object (instance) of a specified class type (ClassType):

    • object_name = ClassType(input1, input2)

    • self is not given an input when creating an object of a specified class

Everything in Python is an Object!#

Data variables are objects#

print(isinstance(True, object))
print(isinstance(1, object))
print(isinstance('word', object))
print(isinstance(None, object))

a = 3
print(isinstance(a, object))

Functions are objects#

print(isinstance(sum, object))
print(isinstance(max, object))
# Custom function are also objects
def my_function():
    print('yay Python!')
    
isinstance(my_function, object)

Class definitions & instances are objects#

class MyClass():
    def __init__(self):
        self.data = 13

my_instance = MyClass()

print(isinstance(MyClass, object))
print(isinstance(my_instance, object))

Object-Oriented Programming#

Object-oriented programming (OOP) is a programming paradigm in which code is organized around objects. Python is an OOP programming langauge.