Style and Format of the Quiz
Logistics
The quiz will be take-home style, and will be released on Tuesday 11/10, 4:45 PM Eastern Time. You will have 75 minutes to complete the quiz with and turn it in by 6:00 PM Eastern Time with a 10 minute grace period for scanning and uploading your diagrams.
International students who opted into the 4:45 AM Eastern Time quizzing will begin theirs via the INTL Gradescope account at 08:45 GMT 11/11 and will similarly have 75 minutes, until 10:00 GMT, with a 10 minute grace period.
The format of this quiz will be similar to what you can expect on the final and should serve as good practice for it. The focus will be on conceptual questions and diagramming, rather than code-writing.
You will need to have some sort of app to take pictures of your diagram to upload to Gradescope.
What topics should I know for the quiz?
- Anything previously covered
- Lists and Dicts can reappear on this, now that we know that they can be used as attributes in classes
- OOP covered on the last quiz will definitely help understand the more advanced concepts we went into
- Everything we learned so far about memory diagrams will still be crucial as this quiz covers advanced memory diagrams
- Classes and Objects - New Topics
- Constructors /
__init__
- Methods
- The
self
parameter
- Constructors /
- Recursion Concepts
- This will also include knowledge on recognizing a
Node
and how to track examples of these “linked lists.”- Note: There won’t be any code writing related to recursion. Just know the concepts well enough to interpret them on the memory diagram questions!
- This will also include knowledge on recognizing a
- Memory Diagrams - New Topics
- Drawing frames for class constructors
- Drawing frames for class methods
- How to track recursion
Classes and Objects - New
Constructors
Before the quiz, we got a brief overview of classes and objects, only knowing that they have can attributes of different types. However, there is so much more to a class than its attributes!
Notice how we declared classes before:
We have a default value even declared for is_graduating. However, if we wanted to change this attribute’s value, or make sure that name and gpa have values, we have to assign them like so:
student_1: Student = Student()
student_1.name = "Jason"
student_1.gpa = 3.42
student_1.is_graduating = True
This seems easy right? However, what if I had multiple students?
student_1: Student = Student()
student_2: Student = Student()
student_3: Student = Student()
student_1.name = "Jason"
student_1.gpa = 3.42
student_1.is_graduating = True
student_2.name = "Julia"
student_2.gpa = 3.83
student_2.is_graduating = False
student_3.name = "JJ"
student_3.gpa = 3.53
student_3.is_graduating = True
This can get very annoying with many, many objects. For every new Student
we declare, we need 3 lines of code just to set its attributes.
What if there was a way to initialize these objects’ attributes in one line? Even better, what if we initialize them in the same line the object is constructed?
This is where constructors come to the rescue! Constructors are defined with the __init__
name in Python and no return type is specified. In addition, a self
parameter is defined.
What is self
? It is a parameter used by class constructors and methods (which we will talk about later) that is used to refer to the specific object of its class. When calling constructors and methods, we NEVER pass in an argument for this self
parameter.
Let’s look an example that simplifies our way of doing things above:
class Student:
name: str
gpa: float
is_graduating: bool
def __init__(self, name: str, gpa: float):
self.name = name
self.gpa = gpa
self.is_graduating = False
One thing to note is that self does NOT have a type declared. It is already implied that the object in question is of the class type it is currently in, so it does not need a type.
Another thing is that we have two name
s in this that we need to be concerned about. The reason why this is allowed though is because self.name
and name
are completely separate. self.name
refers to the object’s name property, while name
refers to the parameter that is passed in.
We also have our default value for is_graduating moved to our constructor as it is essentially doing the same as before. In this case, we decided not to have another parameter by choice, and this could be the case in the problems he asks on the quiz. Do not assume that any attribute will have a parameter passed into the constructor, unless explicitly said in the instructions.
Now let’s actually use this constructor for our three students:
student_1: Student = Student("Jason", 3.42)
student_2: Student = Student("Julia", 3.83)
student_3: Student = Student("JJ", 3.53)
Look at how much code this saves! A few things to note is that we didn’t have to make any reference to __init__
when using the constructor. We actually just use the same syntax as we did when we didn’t have a constructor, but this time we have formal parameters.
What do we mean by formal parameters? Well we did have self
declared as our first ‘parameter’ in __init__
, but this is not a formal parameter that a call to the constructor (or methods as we will see) would require. This is because it is alrady implied which object you’re using as self
, the one you’re creating or one that you called a method on.
But it looks very similar to what we had last time, but this time less lines of code and we are actually filling the parentheses after the class name. With empty parenthesis, there is an implied constructor of:
But as you can see, the __init__
here is entirely useless in this case so Python allows us to not have to write it unless we need it.
Methods
Before, we have just been initializing and assigning attributes. However, objects can do so much more than this!
Think about it like this: if we were to represent a car as a class, it is not sufficient just to have describe a car with its color, make, model, year, mileage, etc.
These are all characteristics of the car. But if we wanted the car to actually do something. Like drive? Or get repainted? We can represent these actions through class methods!
Using the Student
example from before, in the event a student’s just finished a class or even declared graduation, we can have a method that can reflect that change, rather than manually manipulating the attributes in main
.
class Student:
name: str
gpa: float
is_graduating: bool
def __init__(self, name: str, gpa: float):
self.name = name
self.gpa = gpa
self.is_graduating = False
def add_class_grade(self, grade: str) -> None:
"""Once you have completed a class, add the GPA and take the average!"""
new_class_gpa: float
if grade == "A":
new_class_gpa = 4.0
elif grade == "A-":
new_class_gpa = 3.67
elif grade == "B+":
new_class_gpa = 3.33
... # Other possible grades
else:
new_class_gpa = 0.0
self.gpa = (self.gpa + new_class_gpa) / 2
We can see that the method here, add_class_grade
, can be useful if a Student
needed their GPA updated following a class they took. From main, a call to update this would look like:
student_1: Student = Student("Jason", 3.42)
# Jason got a B in COMP110
student_1.add_class_grade("B")
print(str(student_1.gpa)) # Prints 3.21
Notice how we only needed to pass in the letter grade, and the method does the rest of the work for us! We do not need to pass in anything for self
, only the formal parameter of grade
is needed.
This is by the way a terrible way to update your current GPA. But the point is, for manipulating the state of Student
objects, methods are really useful!
Methods can also return a value! Let’s say we want to print a student’s status, but we want a method to do the work for us so we can just call that method for multiple objects:
class Student:
name: str
gpa: float
is_graduating: bool
def __init__(self, name: str, gpa: float):
self.name = name
self.gpa = gpa
self.is_graduating = False
def add_class_grade(self, grade: str) -> None:
"""Once you have completed a class, add the GPA and take the average!"""
new_class_gpa: float
if grade == "A":
new_class_gpa = 4.0
elif grade == "A-":
new_class_gpa = 3.67
elif grade == "B+":
new_class_gpa = 3.33
... # Other possible grades
else:
new_class_gpa = 0.0
self.gpa = (self.gpa + new_class_gpa) / 2
def get_status(self) -> str:
"""Returns the status of the student in the form of a string."""
result: str = f"{self.name} has a GPA of {self.gpa} and "
if self.is_graduating:
result = result + " is graduating."
else:
result = result + " is not graduating."
return result
In get_status
, we have no formal parameters, and therefore it only has self
as a parameter. However, we do have a return value here!
If we call this on all students, we can easily get the statii of all students we have. If we had a List of Student
objects, we can easily iterate through that List and print the statii.
student_1: Student = Student("Jason", 3.42)
student_2: Student = Student("Julia", 3.83)
student_3: Student = Student("JJ", 3.53)
students: List[Student] = [ student_1, student_2, student_3 ]
for student in students:
print(student.get_status())
If you test this code above in a main function, you will see that the following will get printed:
Jason has a GPA of 3.42 and is not graduating.
Julia has a GPA of 3.83 and is not graduating.
JJ has a GPA of 3.53 and is not graduating.
This is the power of using methods, and we will delve into how to diagram all this code above in a later section!
Recursion Concepts
Although the quiz will only require you to trace through code with recursive functions, it still important to know how to recognize it, and what things to look out for when following the program.
Recursion is when a function calls itself at some point in its code.
def factorial(x: int) -> int:
"""Returns x factorial (x * (x-1) * (x-2) ... * 1)."""
if x <= 1:
return 1
else:
return x * factorial(x - 1)
We see above that in the else case, the factorial function will call itself, passing x - 1
as the parameter. This is what we call the recursive case.
In any recursive function, it is crucial that any call made to the function can eventually reach an end, or a base case.
For example, if we have this:
def factorial(x: int) -> int:
"""Returns x factorial (x * (x-1) * (x-2) ... * 1)."""
return x * factorial(x - 1)
print(factorial(2))
We expect 2 factorial to be 2, but this is what happens:
factorial(2) is called, so we need to evaluate it
It returns 2 * factorial(2 - 1)
Now we must evaluate factorial(1)
It returns 1 * factorial(1 - 1)
Now we must evaluate factorial(0)
It returns 0 * factorial(0 - 1)
Now we must evaluate factorial(-1)
It returns -1 * factorial(-1 - 1)
Now we must evaluate factorial(-2)
....
This will keep going and going until the stack is overflowed (if we think about the memory diagram for this, the stack will be infinitely long).
This is why a base case is necessary.
def factorial(x: int) -> int:
"""Returns x factorial (x * (x-1) * (x-2) ... * 1)."""
if x <= 1:
return 1
else:
return x * factorial(x) # Just took out the minus 1 for fun :)
Now this should work, right?
Let’s try factorial(2)
again:
factorial(2) is called, so we need to evaluate it
It returns 2 * factorial(2)
Now we must evaluate factorial(2)
It returns 2 * factorial(2)
Now we must evaluate factorial(2)
It returns 2 * factorial(2)
Now we must evaluate factorial(2)
....
Well the base case clearly didn’t help. Looks like getting rid of the minus 1 for fun didn’t work. Not only does a recursive function need a base case, but it will need to make progress towards the base case.
The minus 1 we had before allowed us to make progress towards reaching our base case so our program can finally return.
Let’s go back to what we originally had:
def factorial(x: int) -> int:
"""Returns x factorial (x * (x-1) * (x-2) ... * 1)."""
if x <= 1:
return 1
else:
return x * factorial(x - 1)
print(factorial(4))
factorial(4) is called, so we need to evaluate it
1. It returns 4 * factorial(4 - 1)
Now we must evaluate factorial(3)
2. It returns 3 * factorial(3 - 1)
Now we must evaluate factorial(2)
3. It returns 2 * factorial(2 - 1)
Now we must evaluate factorial(1)
4. With the base case of 1 being <= 1, we return 1
Now we process backwards, starting with step 3
Return 2 * 1 = 2
Going back to step 2
Return 3 * 2 = 6
Going back to step 3
Return 4 * 6 = 24
Recursion can be powerful, but confusing when we trace through it without a memory diagram. Later we will go over how to draw a memory diagram for this.
To quickly go over linked lists, known as Nodes
, it is best understood as a class that has an attribute referencing itself:
This is very similar to how function can call itself, so this is why we call Node
s a recursive data type.
The data
attribute is the data it contains, where as next
is really a pointer to another node.
These linked lists are very powerful with recursive functions, which we will see here and when we trace through a memory diagram.
class Node:
data: int
next: Optional[Node]
def __init__(self, data: int, next: Optional[Node]):
self.data = data
self.next = next
def product(head: Optional[Node]) -> int:
if head is None: # Base case where the linked list ends
return 1
else:
return head.data * product(head.next) # Recursive case to call on the next node
Here, we continue down the list by recursively calling product
on the next node of the list. We will go over how this looks on a memory diagram later.
Memory Diagrams - New
From before the last quiz, you all have a basic understanding of memory diagrams and tracing through a program with a couple of function calls. You all even had problems where Dicts were involved, and you saw your first objects on the quiz!
However, with our new understanding of classes having constructors and objects, we have some new things to add to our environment diagrams.
Here are some key tips to success with memory diagrams. If you take ANYTHING from this review doc, I would say this is the part to pay the most attention to in order to avoid silly mistakes:
Draw your globals frame and ALL classes and functions declared, including main. The only things you don’t indicate from the program onto your diagram are the imports, and the if block for calling
main()
. Everything else in some way will be indicated on your diagram as part of your globals.- Every frame except for globals NEEDS a return value and return address indicated. Do NOT lose points just because you forget to do this.
- The
main
frame will always have a null return value, which you can just draw a circle with a line through. - For every function call that happens, once you draw the frame, write the line number of the function call (not the definition) as the return address on that frame.
- The
- Every time a new object is created, check to see if its defined class has a defined
__init__
, also known as the constructor. If so, you must add a frame on the stack for this, titled<CLASS_NAME>#__init__
. Same notation applies for method calls:<CLASS_NAME>#<method_name>
.- Basically, if you’re not writing hashtags when calling class constructors/methods, you’re doing something wrong :)
On EVERY frame that has a hashtag, i.e. every frame with
something#__init__
orsomething#<method_name>
, DO NOT forget to put theself
parameter. I cannot stress enough that this is super important. Just like any function parameter name gets written down on its frame, theself
parameter, although an argument isn’t passed in, should get written in the frame and point to the object that is being created/called upon.In any frame that’s created for the constructor, i.e. any frame that is named
something#__init__
, the return value will point to the SAME object asself
. Golden rule: In all__init__
frames in a memory diagram, RV will point to the same object asself
. This should make sense as a constructor creates a new object, which ends up being whatself
refers to as well.In recursive calls, DO NOT rewrite values in the same frame. Draw a BRAND NEW frame for every recursive call that’s made.
I review in videos how to trace through the examples from above in a memory diagram. This includes the example with Student
, the example with factorial
, and the example with product
with Node
.
Here is the example with Student
. Here is the example with factorial
Here is the example with product
.
Important Note regarding For… in loops in Memory Diagrams
Something extremely important to understand about for in loops is that when we define the counter variable within the for statement, that also gets defined on the memory diagram even though no type is explictly declared. Please make sure that on the quiz, that whenever we have something like:
Make sure you also define x on the frame and update that on your diagram as you go through each item on the list. Just like how you would declare a variable i whenever we have a while loop, you do the same for the variable declared in the for .. in statement.
Another note is that when we use the actual item versus the index in our for in loop, i.e.:
versus
On the heap, the first set of code (where x
represents the item on the list) nothing should change on the heap! This is because we define x
itself to be each item on that list, and therefore doing x = x + 1
would only affect the x
variable in the diagram. The variable x
will not always be a direct reference to the item on the list itself. Remember, strings, ints, floats, and bools are not reference types, so a List of these variable types would mean that a for loop directly accessing each of these elements would not be a reference. Again, this is why only x
in the first example changes, not the actual element on the xs
list.
On the other hand, if we used index accessing like we do in the second set of code, that will change the element on the heap List, since we are directly referring to the list xs
when using the bracket notation.
How do I best prepare for the quiz?
- For the memory diagrams, definitely work on the practice problems we assign, but also look back at problems done in lecture to ensure you understand the concepts.
- To emphasize again about memory diagrams, it is super important to go line by line, starting from the call to
main
. Try to understand when we make a new frame, when we draw an object on the heap, when we draw arrows, etc. - Tracing through recursive examples from class and the exercises would be a great thing to do as we only just started talking about them.
- Look at your current project and try to really study the concepts you learn from it, such as using self, calling other methods, using other class objects within a class, etc.
- TA’s are here to help in office hours, please feel free to go through practice problems with them, they will be glad to walk you through understanding these concepts!