Quiz 04 Practice Questions

Change Log

Dec 2: Point class changed to have int attributes from float attributes. float attributes were used in class but the definition has been changed for these problems to be consistent with how the Rectangle class uses them. Additionally, the answer for the Knight and Castle memory diagram has been updated to correct the labels of some method call frames (Guard was accidentally used as the class name at the top of a few of the function call frames instead of Knight).

Magic Methods

OOP Review

  1. What does self refer to in Python classes?

  2. Similar to how a function is first defined then called, a class is first defined then ____.

  3. When a method is called, do you have to pass an argument to the self parameter?

  4. When is self used outside of a class definition?

SOLUTIONS

  1. self refers to the current instance of the class that the methods will operate on if those methods are called in the future on an instance of that class.

  2. Instantiated

  3. No! When you call the constructor for a class, self is automatically made by python in order for the rest of the constructor to finish making the object. In the case of other methods, python knows that self is the object that you called the method on, often the variable name that comes before the method call (e.g. for my_point.shift_y(1.0), self is my_point).

  4. self is not used outside of a class definition. Outside of a class definition you use the name of the variable storing an object to refer to it.

Conceptual

  1. Consider the following code snippet:

    1     class Point:
    2         x: int
    3         y: int
    4
    5         def __init__(self, x: int, y: int):
    6             self.x = x
    7             self.y = y
    8
    9         def __str__(self) -> str:
    10            return f"({self.x}, {self.y})"
    11
    12        def __repr__(self) -> str:
    13            return f"Point({self.x}, {self.y})"
    14
    15    
    16    my_point: Point = Point(1, 2)
    17    my_str: str = f"My point is {my_point}!"

    Would the line of code that creates my_str also call the Point class’s __str__ method?

  2. In order to call a magic method, you use its name (e.g. __str__) directly just like any other method (T/F).

  3. When creating a __str__ method, what do you generally want to return? How is this different than what the __repr__ method returns?

SOLUTIONS

  1. Yes it would! In order to create a str object that includes my_point like this in the f-string, the __str__ method of my_point is implicitly called.

  2. False! It is almost always implicitly called such as in the previous question, or such as when the __init__ method is called using the class name.

  3. The __str__ returns a human-readable string that represents the object, usually including its attributes. The __repr__ method returns a string representation as well, but for the python interpreter and for debugging purposes. Generally the string you create is made to look like a call to the constructor that would construct the object you are representing.

Code Writing

  1. Consider the following incomplete class definition along with the previously defined Point class:

    1     class Rectangle:
    2         bottom_left: Point
    3         bottom_right: Point
    4         top_left: Point
    5         top_right: Point
    6
    7         def __init__(self, bl: Point, br: Point, tl: Point, tr: Point):
    8             self.bottom_left = bl
    9             self.bottom_right = br
    10            self.top_left = tl
    11            self.top_right = tr
    12
    13        def area(self) -> int:
    14            """Returns the area of the rectangle."""
    15            ...
    16
    17        def perimeter(self) -> int:
    18            """Returns the perimeter of the rectangle."""
    19            ...

    1.1. Fill in the methods for area and perimeter using the four Point attributes of the Rectangle class.

    1.2. (Challenge Question) How could you equivalently write this class definition while using only two attributes? How would your area and perimeter methods change with only two attributes?

    1.3. Write a __repr__ method for Rectangle that for any Rectangle object would show up in Trailhead as how the constructor appears. For example, my_rect: Rectangle = Rectangle(Point(0, 0), Point(1, 0), Point(0, 1), Point(1, 1)) should show up in Trailhead as: Rectangle(Point(0, 0), Point(1, 0), Point(0, 1), Point(1, 1))

    Hint: Remember when you used repr() in class.

    1.4. (Challenge Question) Write a __str__ method for Rectangle that works like in the following example:

    >>> my_rect: Rectangle = Rectangle(Point(0, 0), Point(1, 0), Point(0, 1), Point(1, 1))
    >>> print(my_rect)
    (0, 1) (1, 1)
    (0, 0) (1, 0)
    Area: 1
    Perimeter: 4

    Hint: Use "\n" to add new lines! Example: >>> print("Hello!\nHello again!") Hello! Hello again!

SOLUTIONS

from __future__ import annotations

# Included for context, and so you can run it yourself!
class Point:
    x: int
    y: int

    def __init__(self, x: int, y: int):
        self.x = x
        self.y = y

    def __str__(self) -> str:
        return f"({self.x}, {self.y})"

    def __repr__(self) -> str:
        return f"Point({self.x}, {self.y})"

class Rectangle:
    bottom_left: Point
    bottom_right: Point
    top_left: Point
    top_right: Point

    def __init__(self, bl: Point, br: Point, tl: Point, tr: Point):
        self.bottom_left = bl
        self.bottom_right = br
        self.top_left = tl
        self.top_right = tr

    # 1.1

    def area(self) -> int:
        """Returns the area of the rectangle."""
        x_length: int = self.bottom_right.x - self.bottom_left.x
        y_length: int = self.top_left.y - self.bottom_left.y
        return x_length * y_length

    def perimeter(self) -> int:
        """Returns the perimeter of the rectangle."""
        x_length: int = self.bottom_right.x - self.bottom_left.x
        y_length: int = self.top_left.y - self.bottom_left.y
        return (x_length * 2) + (y_length * 2)

    # 1.3

    def __repr__(self):
        return f"Rectangle({repr(self.bottom_left)}, {repr(self.bottom_right)}, {repr(self.top_left)}, {repr(self.top_right)})"

    # 1.4

    def __str__(self) -> str:
        return f"{self.top_left} {self.top_right}\n{self.bottom_left} {self.bottom_right}\nArea: {self.area()}\nPerimeter: {self.perimeter()}"

For question 1.3, you can represent a rectangle with just two of its opposite corners, since the bottom left’s x coordinate should be the same as it’s top left x coordinate, and the same with the bottom and top right’s x. Similarly, the bottom left’s y coordinate should be the same as the bottom right’s y coordinate, and the same with the top left and top right’s y.

The area and perimeter methods you wrote previously might be the same, but likely are not since the most intuitive way to measure the x and y length of a rectangle would be on the same side. But by the same reasoning as we used to know where the other two corners are, we can calculate the x and y lengths like how it is shown below.

class Rectangle:
    bottom_left: Point
    top_right: Point

    def __init__(self, bl: Point, tr: Point):
        self.bottom_left = bl
        self.top_right = tr

    def area(self) -> int:
        """Returns the area of the rectangle."""
        x_length: int = self.top_right.x - self.bottom_left.x
        y_length: int = self.top_right.y - self.bottom_left.y
        return x_length * y_length

    def perimeter(self) -> int:
        """Returns the perimeter of the rectangle."""
        x_length: int = self.top_right.x - self.bottom_left.x
        y_length: int = self.top_right.y - self.bottom_left.y
        return (x_length * 2) + (y_length * 2)

Memory Diagram

  1. Diagram the following code snippet:
1     class Knight:
2         """A medieval Knight."""
3         name: str
4
5         def __init__(self, name: str):
6             self.name = name
7
8         def __str__(self) -> str:
9             return f"Sir {self.name}"
10
11    class Castle:
12        """A medieval castle with a drawbridge for crossing a surrounding moat and a guarding knight."""
13        guard: Knight
14        drawbridge_up: bool
15
16        def __init__(self, guard: Knight, bridge_up: bool):
17            self.guard = guard
18            self.drawbridge_up = bridge_up
19
20        def __str__(self) -> str:
21            if self.drawbridge_up:
22                return f"Guarded by {self.guard} and closed to outsiders!"
23            else:
24                return f"Guarded by {self.guard} but open to all!"
25            
26        def open(self) -> None:
27            if self.drawbridge_up:
28                print("Let down the bridge!")
29                self.drawbridge_up = False
30            else:
31                print("Already open!")
32
33        def close(self) -> None:
34            if not self.drawbridge_up:
35                print("Pull up the bridge!")
36                self.drawbridge_up = True
37            else:
38                print("Already closed!")
39
40
41    lancelot: Knight = Knight("Lancelot")
42    my_castle: Castle = Castle(lancelot, False)
43    print(my_castle)
44    my_castle.close()
45    print(my_castle)

SOLUTION

Memory diagram of code listing with Guard and Castle classes.

Recursive Structures

Any questions that reference the Node class are referring to a class defined in the following way:

    from __future__ import annotations

    class Node:
        value: int
        next: Node | None

        def __init__(self, val: int, next: Node | None):
            self.value = val
            self.next = next

        def __str__(self) -> str:
            rest: str
            if self.next is None:
                rest = "None"
            else:
                rest = str(self.next)
            return f"{self.value} -> {rest}"

Multiple Choice

  1. (Select all that apply) Which of the following properties of a recursive function will ensure that it does not have an infinite loop?

    1. The function calls itself in the recursive case.

    2. The recursive case progresses towards the base case.

    3. The base case returns a result directly (it does not call the function again).

    4. The base case is always reached.

    5. None of the above

  2. (Fill in the blank) A linked list in python consists of one or more instances of the _____ class.

    1. list

    2. int

    3. Node

    4. None

    5. None of the above

  3. (True/False) Attempting to access the value or next attribute of None will result in an error.

  4. (True/False) There is no way to traverse to the start of a linked list that has multiple Nodes given only a reference to the last Node.

SOLUTIONS

  1. B, C, and D. A is true of all recursive functions, but does not guarantee that there won’t be an infinite loop.

  2. C

  3. True, attempting to access any attributes of None will result in an error since it has no attributes.

  4. True, Nodes only know about the Node “in front” of them, or the next Node, so you cannot move backwards in a linked list.

Code Writing

  1. Write a recursive function (not a method of the Node class) named recursive_range with start and end int parameters that will create a linked list with the Nodes having values counting from start to end, not including end, either counting down or up. The function signature is below to get you started. Note: The example of recursive_range in class was different in that it only incremented or counted up from start to end, but this function should go either way depending on what start and end are.

    def recursive_range(start: int, end: int) -> Node | None:

  2. Write a recursive method of the Node class named append that has parameters self and new_val which is of type int, and this method should create a new Node at the end of the linked list and return None. In other words, the last Node object before this method is called will have a next attribute of None, but after this method is called, it should have a next attribute equal to a Node object with value new_val and next attribute being None (since that new node is now the last Node in the linked list).

  3. Write a recursive method of the Node class named get_length that has parameters self and count which is of type int, which if you were to call with a count argument of 0, would return the length of the linked list starting with self (not including None). Hint: Use count to keep track of a Node count between function calls. How would you write this method as an iterative function (with no count parameter)?

SOLUTIONS

  1. Recursive range has two base cases, and the one that is used depends on if start is greater than or less than end.

        def recursive_range(start: int, end: int) -> Node | None:
            if start == end:
                return None
            elif start < end:
                return Node(start, recursive_range(start + 1, end))
            else:
                return Node(start, recursive_range(start - 1, end))
  2. Here is one way to make the append method:

        def append(self, new_val: int) -> None:
            if self.next is None:
                self.next = Node(new_val, None)
            else:
                self.next.append(new_val)
  3. Here are two possibilities:

    def get_length(self, count: int) -> int:
        if self.next is None:
            return count + 1
        else:
            return self.next.get_length(count + 1)
    def get_length(self, count: int) -> int:
        count += 1
        if self.next is None:
            return count
        else:
            return self.next.get_length(count)

Short Answer

  1. Based on the following code snippet, what would be the output of the following lines of code given in parts 1.1-1.4?

        from __future__ import annotations
    
        # Node class definition included for reference!
        class Node:
            value: int
            next: Node | None
    
            def __init__(self, val: int, next: Node | None):
                self.value = val
                self.next = next
    
            def __str__(self) -> str:
                rest: str
                if self.next is None:
                    rest = "None"
                else:
                    rest = str(self.next)
                return f"{self.value} -> {rest}"
    
        x: Node = Node(4, None)
        y: Node = Node(8, None)
    
        x.next = y
    
        z: Node = Node(16, None)
    
        z.next = x
    
        x = Node(32, None)

    1.1. print(z.next.next.value)

    1.2. print(y.next)

    1.3. print(x)

    1.4. print(z)

SOLUTIONS

  1. Question 1 answers:

    1.1. 8

    1.2. None

    1.3. 32 -> None

    1.4. 16 -> 4 -> 8 -> None

Memory Diagram

  1. Create a memory diagram of the following code snippet:

    1     """A messy linked list..."""
    2
    3     from __future__ import annotations
    4 
    5     # Node class definition included for reference!
    6     class Node:
    7         value: int
    8         next: Node | None
    9 
    10        def __init__(self, val: int, next: Node | None):
    11            self.value = val
    12            self.next = next
    13
    14        def __str__(self) -> str:
    15            rest: str
    16            if self.next is None:
    17                rest = "None"
    18            else:
    19                rest = str(self.next)
    20            return f"{self.value} -> {rest}"
    21
    22    knight: Node = Node(3, None)
    23    bishop: Node = Node(2, knight)
    24    rook: Node = Node(1, bishop)
    25    print(rook)
    26    castle: Node = Node(0, bishop)
    27    print(castle)

SOLUTION

Memory diagram of code listing with Guard and Castle classes.

Contributor(s): Benjamin Eldridge