Lab 07 - Classes
Due by 11:59pm on 2023-08-28.
Starter Files
Download lab07.zip. Inside the archive, you will find starter files for the questions in this lab.
Topics
Object-Oriented Programming (OOP)
Object-oriented programming (OOP) is a programming paradigm that allows us to treat data as objects, like we do in real life.
For example, consider the class Student. Each of you are a student, so that means you would be an instance of this class or an object.
Details that all CS 111 students have, such as name, are called instance variables. Every student has these variables, but their values differ from student to student.
A variable, whose value is shared among all instances of Student is known as a class variable. For example, the max_slip_days attribute is a class variable because it is a property of all students. Every student gets the same maximum number of slip days.
All students are able to do homework, attend lecture, and go to office hours. When functions belong to a specific object, they are called methods. In this case, these actions would be methods of Student objects.
Example implementation:
class Student:
max_slip_days = 3 # This is a class variable. Every student has the same max slip days
def __init__(self, name, major): # This is called a constructor
self.name = name # This is an instance variable. Students may have unique names
self.major = major
self.energy = 10
self.understanding = 0
print("Added", self.name)
def do_homework(self): # This is a method
if self.energy <= 0:
print("Too tired!")
self.understanding -= 1
else:
print("Wow, that was interesting!")
self.understanding += 1
self.energy -= 1
Notice, if you want to ever access an attribute or call a method from an object you have to use dot notation. For example, from outside of the class:
>>> elle = Student("Elle", "Law")
>>> elle.name
"Elle"
>>> elle.energy
10
>>> elle.do_homework()
Wow, that was interesting!
>>> elle.energy
9
If you want to reference and object's attribute or method from within the class, you have to use something like
self.understanding where self refers to an object's own self. (Refer back to the code above if needed.)
Try doing Q1: WWPD: Classes.
__str__ and __repr__ Dunder Methods
Dunder methods are special methods that allow your class and objects to interact with existing builtin Python functions.
For this lab, we will focus on the __str__ and __repr__ dunder methods although others do exist such as the
__add__, __mul__, and __len__ dunder methods.
Implementing a __str__ dunder method
allows our class to output some human readable text when called by the str() or print() functions. This dunder
method should return a string containing the cotent to be outputed.
For example, if we add the following code to our Student class:
def __str__(self):
return f"{self.name} is a {self.major} major."
We could now call str() on a student object and it would retrieve the string provided by the __str__ dunder
method. Additionally, print(), when dealing with objects, tries to find a __str__ dunder method. If print()
finds one, it prints out the string provided by the __str__ dunder method.
>>> elle = Student("Elle", "Law")
>>> str(elle)
'Elle is a Law major.'
>>> print(elle)
Elle is a Law major.
__repr__ similarly outputs text, but text that is more machine readable. It will most often be a string containing
how the object was created.
def __repr__(self):
return f"Student('{self.name}', '{self.major}')"
>>> elle = Student("Elle", "Law")
>>> repr(elle)
"Student('Elle', 'Law')"
Required Questions
Q1: WWPD: Classes
Try the WWPD questions for the following code.
class Student:
max_slip_days = 3 # This is a class variable. Every student has the same max slip days
def __init__(self, name, staff):
self.name = name # This is an instance variable. Students may have unique names
self.understanding = 0
staff.add_student(self)
print("Added", self.name)
def visit_office_hours(self, staff):
staff.assist(self)
print("Thanks, " + staff.name)
class Professor:
def __init__(self, name):
self.name = name
self.students = {}
def add_student(self, student):
self.students[student.name] = student
def assist(self, student):
student.understanding += 1
def grant_more_slip_days(self, student, days):
student.max_slip_days = days
>>> callahan = Professor("Callahan")
>>> elle = Student("Elle", callahan)
_____
>>> elle.visit_office_hours(callahan)
_____
>>> elle.visit_office_hours(Professor("Paulette"))
_____
>>> elle.understanding
_____
>>> [name for name in callahan.students]
_____
>>> x = Student("Vivian", Professor("Stromwell")).name
_____
>>> x
_____
>>> [name for name in callahan.students]
_____
>>> elle.max_slip_days
_____
>>> callahan.grant_more_slip_days(elle, 7)
>>> elle.max_slip_days
_____
>>> Student.max_slip_days
_____
Q2: Keyboard
We’d like to create a Keyboard class that takes in an arbitrary number of Buttons and stores these Buttons
in a dictionary. This dictionary will be called buttons. The keys in the dictionary will be integers that represent
the button's postition on the Keyboard, and the values will be the respective Button. Fill out the methods in the
Keyboard class according to each description, using the doctests as a reference for the behavior of a Keyboard.
Remember to read the doctests.
Note: To allow our constructor to take an arbitrary amount of arguments, we need to learn some new Python syntax. A
*argsparameter allows a function or method to take in any amount of arguments (although theargspart is not required. What is required is the*in front of the parameter name). Python effectively grabs all of those arguments and puts them into a tuple (a container like lists, but with different properties).*argscan be converted to a tuple collection by sayingargsby itself without an asterisk*symbol.def func(*args):
return args
a_tuple = func("a", 4, "q") # aTuple will now equal -> ("a", 4, "q")
class Button:
def __init__(self, pos, key):
self.pos = pos
self.key = key
self.times_pressed = 0
class Keyboard:
"""A Keyboard takes in an arbitrary amount of buttons, and has a
dictionary of positions as keys, and values as Buttons.
>>> b1 = Button(0, "H")
>>> b2 = Button(1, "I")
>>> k = Keyboard(b1, b2)
>>> k.buttons[0].key
'H'
>>> k.press(1)
'I'
>>> k.press(2) # No button at this position
''
>>> k.typing([0, 1])
'HI'
>>> k.typing([1, 0])
'IH'
>>> b1.times_pressed
2
>>> b2.times_pressed
3
"""
def __init__(self, *args):
________________
for _________ in ________________:
________________
def press(self, info):
"""Takes in a position of the button pressed, and
returns that button's output."""
if ____________________:
________________
________________
________________
________________
def typing(self, typing_input):
"""Takes in a list of positions of buttons pressed, and
returns the total output."""
________________
for ________ in ____________________:
________________
________________
Test your code:
python3 -m pytest test_lab07.py::test_keyboard_press_and_typing
Q3: __str__ and __repr__
Add from scratch the __str__ and __repr__ dunder methods to the Button and Keyboard class.
For the Button class, __repr__ should return a string on how a button was constructed and
__str__ should return a string describing the button's information.
More specifically, your code should output the following:
>>> b1 = Button(0, "H")
>>> repr(b1)
"Button(0, 'H')"
>>> str(b1)
"Key: 'H', Pos: 0"
>>> print(b1)
Key: 'H', Pos: 0 # recall that print() calls str()
Test your code:
python3 -m pytest test_lab07.py::test_button_str_and_repr
For the Keyboard class, __repr__ should return a string on how the keyboard was constructed and __str__
should return a string containing all the keyboard's button information.
Your code should output the following:
>>> b1 = Button(0, "H")
>>> b2 = Button(1, "I")
>>> k = Keyboard(b1, b2)
>>> repr(k)
"Keyboard(Button(0, 'H'), Button(1, 'I'))"
>>> str(k)
"Key: 'H', Pos: 0 | Key: 'I', Pos: 1"
Hint: You can call
repr()andstr()on the buttons to use when solving this problem. For example,repr(b1)orstr(b2).
Test your code:
python3 -m pytest test_lab07.py::test_keyboard_str_and_repr
Potentially Useful Reminders
It may be helpful to know that when dealing with strings, some characters have special meaning such as quotation marks " ".
If you want to tell python to interpret quotation marks literally rather than as the start and end of a string,
you can use back slash \'. For example
>>> print('I\'m cool')
I'm cool
>>> s = 'I\'m cool'
>>> s
"I'm cool"
When iterating through a dictionary, a for loop iterates through the keys:
d = {'a': 0, 'b': 1, 'c': 2}
for key in d:
print(key) # 'a', 'b', 'c'
print(d[key]) # 0, 1, 2
Q3: Add Button
There are a practical problems with our curent implementation:
- We cannot add any more buttons after we have created the keyboard
- If we provide two buttons with the same
pos, then the second button overrides the first button
To solve this problem, add a function called add_button(button) which takes in a button object and adds
the button object to the keyboard; however, if there is already a button at the keyboard's pos,
then do not add it or replace it (do nothing with it).
>>> b1 = Button(0, "H")
>>> b2 = Button(1, "I")
>>> k = Keyboard(b1, b2)
>>> str(k)
"Key: 'H', Pos: 0 | Key: 'I', Pos: 1"
>>> k.add_button(Button(2, "!"))
>>> k.add_button(Button(2, "?"))
>>> str(k)
"Key: 'H', Pos: 0 | Key: 'I', Pos: 1 | Key: '!', Pos: 2"
Now modify your __init__ method or constructor to use add_button.
Test your code:
python3 -m pytest test_lab07.py::test_keyboard_add_button
Submit
If you attend the lab, you don't have to submit anything.
If you don't attend the lab, you will have to submit working code.
Submit the lab07.py file on Canvas to Gradescope in the window on the assignment page.
Additional Info
Static Methods
Let's say we wanted to add a study_group() function that takes in a list of students and increasing the understanding of each student by 2.
def study_group(student_list):
for student in student_list:
student.understanding += 2
Based on our current understanding, since this function does not rely on a student object to use, we should not add it as a method to
in the Student class. We should keep it outside of the class definition.
class Student:
max_slip_days = 3 # This is a class variable. Every student has the same max slip days
def __init__(self, name, major): # This is called a constructor
self.name = name # This is an instance variable. Students may have unique names
self.major = major
self.energy = 10
self.understanding = 0
print("Added", self.name)
def do_homework(self): # This is a method
if self.energy <= 0:
print("Too tired!")
self.understanding -= 1
else:
print("Wow, that was interesting!")
self.understanding += 1
self.energy -= 1
def study_group(student_list):
for student in student_list:
student.understanding += 2
However, since this method does deal with the Student class, it would be nice for organizational purposes to have it somehow
belong to the Student class so it is slightly more clear that study_group() is a function that is supposed to be used for
Student objects.
Fortunately, Python supports a feature where you can call a method from a class without needing to have an object to call it. This is called a
static method. We can place study_group() within the class definition and add the decorator @staticmethod on top of the method definition.
class Student:
max_slip_days = 3 # This is a class variable. Every student has the same max slip days
def __init__(self, name, major): # This is called a constructor
self.name = name # This is an instance variable. Students may have unique names
self.major = major
self.energy = 10
self.understanding = 0
print("Added", self.name)
def do_homework(self): # This is a method
if self.energy <= 0:
print("Too tired!")
self.understanding -= 1
else:
print("Wow, that was interesting!")
self.understanding += 1
self.energy -= 1
@staticmethod
def study_group(student_list):
for student in student_list:
student.understanding += 2
Before making study_group() a static method, we would have to call it like this:
>>> elle = Student("Elle", "Law")
>>> isaih = Student("Isaih", "Computer Science")
>>> max = Student("Max", "Math")
>>> study_group([elle, isaih, max])
Now as a static method, we would have to call study_group() with the class name preprended to it:
>>> elle = Student("Elle", "Law")
>>> isaih = Student("Isaih", "Computer Science")
>>> max = Student("Max", "Math")
>>> Student.study_group([elle, isaih, max])