CS5001 / CS5003:
Intensive Foundations of Computer Science
Lecture 8: Introduction to Classes and OOP

Lecture 8: Midterm Review
- Let's first talk about the midterm exam: great job overall!
- The questions were meant to be challenging but not tricky.
- If you still have questions about the midterm, please email me to chat.
- I want to look at a couple of problems that seemed to be most difficult.
Lecture 8: Midterm Review
- Question 1c
def mystery_c(s1, s2): """ TODO: Explain what the function does :param s1: a string :param s2: a string :return: None Note: For the doctest, assume file.txt contains the following three lines: the cat in the hat green eggs and ham fox in socks >>> mystery_c('file.txt', 'ae') >>> with open('file.txt') as f: ... for line in f: ... print(line[:-1]) TODO: Doctest output (note, the doctest output is just going to be the contents of the file after you run the test) """ with open(s1, "r") as f: lines = f.readlines() with open(s1, "w") as f: for line in lines: f.write(''.join([c.upper() for c in line if c not in s2]))
- Lots of people asked about the doctest: a doctest is just a REPL listing. Lines 11-13 plus your answer make up the doctest in this case.
- Some people missed the fact that all characters that made it through the filter were changed to uppercase.
Lecture 8: Midterm Review
- Question 2: Checksum -- great job!
def checksum(s): """ Returns the sum of all the ASCII values of the characters in the string. :param s: A string :return: The sum of the ASCII values of the string >>> checksum("hello") 532 """ sum = 0 for c in s: sum += ord(c) return sum
- Most students figured this one out, including figuring out a string that would produce the same checksum as 'hello'.
Lecture 8: Midterm Review
- Question 3: Hamming distance -- some solutions were too verbose!
def hamming_distance(s1, s2): """ Returns the Hamming distance for two strings, or None if the two strings have different lengths. :param s1: the first string :param s2: the second string :return: An integer representing the Hamming distance between s1 and s2, or None if the strings have different lengths >>> hamming_distance('GGACG', 'GGTCA') 2 """ if len(s1) != len(s2): return None hd = 0 for c1, c2 in zip(s1, s2): if c1 != c2: hd += 1 return hd
- This was a great time to use the zip function.
- There were other perfectly fine ways to do this problem.
Lecture 8: Midterm Review
- Question 4: Count and Wrap: I saw some tortured solutions
def count_and_wrap(total, wrap_after): """ Prints total number of lines, starting from 0 and wrapping after wrap_after. :param total: an integer :param wrap_at: an integer :return: None >>> count_and_wrap(9, 4) 0 1 2 3 4 0 1 2 3 """ for i in range(total): print(i % (wrap_after + 1))
- This took a bit of thinking to get right, but the solution is straightforward.
- I saw some correct solutions that I had to code up and try before I was convinced they were correct.
Lecture 8: Midterm Review
- Question 5b: multiply recursively
def multiply(a, b): """ Multiplies a and b using recursion and only + and - operators :param a: a positive integer :param b: a positive integer :return: a * b """ if b == 0: return 0 return a + multiply(a, b - 1)
- Remember:
- Base case
- Work towards a solution by making the problem a bit smaller
- Recurse
- Some students counted down a, and others counted down b. Either was fine.
- How could we ensure we are doing the least amount of work?
Lecture 8: Midterm Review
- Least amount of work (a more efficient solution):
def multiply_efficient(a, b): if a < b: return multiply(b, a) if b == 0: return 0 return a + multiply_efficient(a, b - 1)
import timeit print("Timing multiply(10, 900):") print(timeit.timeit(lambda: multiply(10, 900), number=10000)) print() print("Timing multiply(900, 10):") print(timeit.timeit(lambda: multiply(900, 10), number=10000)) print() print("Timing multiply_efficient(900, 10):") print(timeit.timeit(lambda: multiply_efficient(900, 10), number = 10000)) print() print("Timing multiply_efficient(10, 900):") print(timeit.timeit(lambda: multiply_efficient(10, 900), number = 10000)) print()
- We now count down the value that is smallest -- why does this save time?
- We can use Python to test a function (we will learn about lambdas soon):
- This tests the functions by running them 10,000 times in a row
Lecture 8: Midterm Review
import timeit print("Timing multiply(10, 900):") print(timeit.timeit(lambda: multiply(10, 900), number=10000)) print() print("Timing multiply(900, 10):") print(timeit.timeit(lambda: multiply(900, 10), number=10000)) print() print("Timing multiply_efficient(900, 10):") print(timeit.timeit(lambda: multiply_efficient(900, 10), number = 10000)) print() print("Timing multiply_efficient(10, 900):") print(timeit.timeit(lambda: multiply_efficient(10, 900), number = 10000)) print()
Timing multiply(10, 900): 2.596630092 Timing multiply(900, 10): 0.017811094999999888 Timing multiply_efficient(900, 10): 0.020884906000000036 Timing multiply_efficient(10, 900): 0.019478217000000075
- The original function was super-slow, because it had to count down from 900, which takes time.
- Also: we couldn't go to 1000, because we would have a stack overflow
- The efficient solution is fast no matter what
Lecture 8: Introduction to Classes and OOP
- This week, we are going to start talking about classes and object oriented programming.
- Object Oriented Programming uses classes to create objects that have the following properties:
- An object holds its own code and variables
- You can instantiate as many objects of a class as you'd like, and each one can run independently.
- You can have objects communicate with each other, but this is actually somewhat rare.
- You saw an example of a class in last week's lab
- The Ball class is an object
- You can create as many balls as you want
- Each can have its own attributes
- color
- direction
- size
- etc.

Lecture 8: Creating a class creates a type
- When we create a new class, we actually create a new type. We have only used types that are built in to python so far: strings, ints, floats, dicts, lists, tuples, etc.
- Now, we are going to create our own type, which we can use in a way that is similar to the built-in types.
- Let's start with the Ball example, but let's make it a bit simpler than we saw it in the lab. In fact, let's make it really simple (in that it doesn't do anything):
class Ball: """ The Ball class defines a "ball" that can bounce around the screen """
>>> class Ball: ... """ ... The Ball class defines a "ball" that can bounce around the screen ... """ ... >>> print(Ball) <class '__main__.Ball'> >>>
- In the REPL:
Notice that the full name of the type is '__main__.Ball'
Lecture 8: Creating a class creates a type
- Once we have a class, we can create an instantiation of the class to create an object of the type of the class we created:
>>> class Ball: ... """ ... The Ball class defines a "ball" that can bounce around the screen ... """ ... >>> print(Ball) <class '__main__.Ball'> >>> >>> my_ball = Ball() >>> print(my_ball) <__main__.Ball object at 0x109b799e8> >>>
- Now we have a Ball instance called my_ball that we can use. We can create as many more instances as we'd like:
>>> lots_of_balls = [Ball() for x in range(1000)] >>> len(lots_of_balls) 1000 >>> print(lots_of_balls[100]) <__main__.Ball object at 0x109dc6e10> >>>
- We now have 1000 instances of the Ball type in a list.
Lecture 8: The __init__ method of a class
- Let's make our Ball a bit more interesting. Let's add a location for the Ball, and let's also make a method that draws the ball on a canvas, which is a drawing surface available to Python through the Tkinter GUI (Graphical User Interface)
- We can add functions to a class, too -- they are called methods, and are run with the dot notation we are used to. There is a special method called "__init__" that runs when we create a new class object:
class Ball: """ The Ball class defines a "ball" that can bounce around the screen """ def __init__(self, canvas, x, y): self.canvas = canvas self.x = x self.y = y self.draw() def draw(self): width = 30 height = 30 outline = 'black' fill = 'black' self.canvas.create_oval(self.x, self.y, self.x + width, self.y + height, outline=outline, fill=fill)
- What is this "self" business?
- "self" refers to the instance, and each instance has its own attributes that can be shared among the methods.
- All methods in a class have a default "self" parameter.
- In __init__, we set the parameters to be attributes for use in all the methods.
Lecture 8: The __init__ method of a class
class Ball: """ The Ball class defines a "ball" that can bounce around the screen """ def __init__(self, canvas, x, y): self.canvas = canvas self.x = x self.y = y self.draw() def draw(self): width = 30 height = 30 outline = 'blue' fill = 'blue' self.canvas.create_oval(self.x, self.y, self.x + width, self.y + height, outline=outline, fill=fill)
- The __init__ method is called immediately when we create an instance of the class. You can think of it as the setup, or initialization routine.
- Notice in "draw" that we create regular variables. Those can only be used in the method itself.
- If we want, we can promote those variables to become attributes so different instances can have different values.
Lecture 8: The __init__ method of a class
class Ball: """ The Ball class defines a "ball" that can bounce around the screen """ def __init__(self, canvas, x, y): self.canvas = canvas self.x = x self.y = y self.draw() def draw(self): width = 30 height = 30 outline = 'blue' fill = 'blue' self.canvas.create_oval(self.x, self.y, self.x + width, self.y + height, outline=outline, fill=fill) def animate(playground): canvas = playground.get_canvas() ball = Ball(canvas, 10, 10) canvas.update() // redraw canvas
- Because Tkinter needs some setup, I haven't included it here. But, assume you have an animate function that has a playground parameter that gives you a canvas (see Lab 8 if you want details).
- When we instantiate ball, the __init__ method is called, which sets up the attributes, and then draws the ball on the screen.
Lecture 8: The __init__ method of a class
class Ball: """ The Ball class defines a "ball" that can bounce around the screen """ def __init__(self, canvas, x, y): self.canvas = canvas self.x = x self.y = y self.draw() def draw(self): width = 30 height = 30 outline = 'blue' fill = 'blue' self.canvas.create_oval(self.x, self.y, self.x + width, self.y + height, outline=outline, fill=fill) def animate(playground): canvas = playground.get_canvas() balls = [] for i in range(10) balls.append(Ball(canvas, 30 * i, 30 * i)) canvas.update() // redraw canvas
- We can, of course, create as many balls as we want.

Lecture 8: The __init__ method of a class
class Ball: """ The Ball class defines a "ball" that can bounce around the screen """ def __init__(self, canvas, x, y, width, height, fill): self.canvas = canvas self.x = x self.y = y self.width = width self.height = height self.fill = fill self.draw() def draw(self): self.canvas.create_oval(self.x, self.y, self.x + self.width, self.y + self.height, outline=self.fill, fill=self.fill) def animate(playground): canvas = playground.get_canvas() ball1 = Ball(canvas, 100, 100, 50, 30, "magenta") ball2 = Ball(canvas, 40, 240, 10, 100, "aquamarine") ball3 = Ball(canvas, 200, 200, 150, 10, "goldenrod1") ball4 = Ball(canvas, 300, 300, 1000, 1000, "yellow") canvas.update()
- Now, we can modify each of the ball's position, size, and color independently.
- What could we do if we wanted to give each attribute a default value?
- Just like with regular functions, the __init__ method can accept defaults (see next slide)
Lecture 8: The __init__ method of a class
class Ball: """ The Ball class defines a "ball" that can bounce around the screen """ def __init__(self, canvas, x, y, width=30, height=30, fill="blue"): self.canvas = canvas self.x = x self.y = y self.width = width self.height = height self.fill = fill self.draw() def draw(self): self.canvas.create_oval(self.x, self.y, self.x + self.width, self.y + self.height, outline=self.fill, fill=self.fill) def animate(playground): canvas = playground.get_canvas() ball1 = Ball(canvas, 100, 100) # default size and color ball2 = Ball(canvas, 40, 240, fill="aquamarine") ball3 = Ball(canvas, 200, 200, 150, 10) ball4 = Ball(canvas, 300, 300, 1000, 1000, "yellow") canvas.update()
- Q: Why do we have to say fill="aquamarine" ?
- A: If we leave out default arguments, we have to name any other default arguments
Lecture 8: The __str__ and __eq__ methods of a class
- Besides __init__, there are a couple of other special methods that classes know about, and that you can write:
- __str__
- Returns a string that you can print out that tells you about the instance
- __eq__
- If you pass in two instances, __eq__ will return True if they are the same, and False if they are different
- __str__
- We can define these functions to do whatever we want, but we generally want them to make sense for creating a string representation of the object, and for determining if two objects are equal.
Lecture 8: The __str__ and __eq__ methods of a class
- Before we write the functions, let's see what happens when we try to print a ball, and to determine if two balls are equal:
ball1 = Ball(canvas, 100, 100) # default size and color ball2 = Ball(canvas, 40, 240, fill="aquamarine") ball3 = Ball(canvas, 200, 200, 150, 10) ball4 = Ball(canvas, 300, 300, 1000, 1000, "yellow") ball5 = Ball(canvas, 300, 300, 1000, 1000, "yellow") // same as ball4 canvas.update() print(ball1) print(ball2) print(ball3) print(ball4) print(f"ball4 == ball5 ? {ball4 == ball5}") print(f"ball1 == ball5 ? {ball1 == ball5}")
ball4 == ball5 ? False ball1 == ball5 ? False <__main__.Ball object at 0x10484f1d0> <__main__.Ball object at 0x10484f208> <__main__.Ball object at 0x10484f240> <__main__.Ball object at 0x10484f278>
- This is probably not what we want. ball4 and ball5 should be equal, and when we print out a ball, it isn't very useful.
Lecture 8: The __str__ and __eq__ methods of a class
- Here is an example of the __str__ method for our Ball class:
def __str__(self): """ Creates a string that defines a Ball :return: a string """ ret_str = "" ret_str += (f"x=={self.x}, y=={self.y}, " f"width=={self.width}, height=={self.height}, " f"fill=={self.fill}") return ret_str
- We create a string with the attributes we care to print, and then we return the string.
Lecture 8: The __str__ and __eq__ methods of a class
- Here is an example of the __eq__ method for our Ball class:
def __eq__(self, other): return ( self.canvas == other.canvas and self.x == other.x and self.y == other.y and self.width == other.width and self.height == other.height and self.fill == other.fill )
- We perform the comparison on the two different balls, and return True if they are the same, and False otherwise.
Lecture 8: The __str__ and __eq__ methods of a class
- There are other, related methods you can also create:
- __ne__ (not equal). In Python 3, we don't usually bother creating this, because the language just treats != as the opposite of ==.
- __lt__ (less than)
- __le__ (less than or equal to)
- __gt__ (greater than)
- __ge__ (greater than or equal to)
- There isn't necessarily a good way to determine if a ball is "less than" another ball, but for some objects it makes more sense.
CS5001 / CS5003: Intensive Foundations of Computer Science PDF of this presentation Lecture 8: Introduction to Classes and OOP
Lecture 8 - Introduction to Classes and OOP
By Chris Gregg
Lecture 8 - Introduction to Classes and OOP
- 1,639