Day 15: Debugging

back · home · slides · CMSC 201 (Fall 2024) @ UMBC · fixing our code

CMSC 201 Day 15: Debugging

Agenda:

  • Homework and such
  • Methods for designing code
  • How to properly comment code
  • Modularity and testing code
  • Recursion example and debugging
  • Hex, binary, and decimal conversions

Goal: Learn about clean code, no smell!

Your lecturer's email is: sdonahue@umbc.edu (Shane Donahue), office hours are canceled this week :( You can still email me or your TA with questions. Thank you to Dana and Prof. Hamilton for assistance with these slides!

Intro

Hello! I/we am/are your guest lecturer(s) :)

Assignments and such

Project 1 (pytzee) due this Friday (Nov 1) at 11:59PM! That's in like 1-2 days!

HW6 will be released this Friday... Recursion, look forward to it :)

Methods for Designing Code

Writing code is hard! Can we apply some methodology to make it easier? Or at least, more structured?

Top Down Design

Cooked tofu meal https://17ddblog.com/wp-content/uploads/2023/01/tofubroccolistirfrynewhero1-720x405.jpg

If we know what the end goal of our program is, we can break it up into small parts.

For example, if I wanted to design a program that tell me how to cook this:

if __name__ == "__main__":
    cook_meal()

That's the top level goal.

def cook_meal():
    # first I need to drain the tofu
    drain_tofu()
    # then I need to cut the tofu
    cut_ingredients()
    # then I need to heat the oven
    while get_oven_temp() < correct_temp:
        heat_oven()
    # etc...

if __name__ == "__main__":
    cook_meal()

The point here is that we define major, top-level goals, since I know the end goal, and from there we iteratively complete each sub-component until the entire program was done. (This is the "iterative design" that we've been using throughout the semester.) We may use pseudocode like the above, or comments, to help plan out our programs.

Bottom Up Design

Uncooked tofu https://www.unlockfood.ca/EatRightOntario/media/Website-images-resized/Tofu-v-2-resized.jpg

Bottom up design is complementary to top down design. It will be used in projects where we aren't quite sure of the end goal, or want to iterate on our designs before putting all the pieces together.

Let's say I want to add to my meal-maker bot another function that generates sauces. I'm not quite sure how this will look, but I know I have ingredients...

And I know that I want to add them or combine them depending on their flavor (go back and add (iterate) spicy, fat, salt).

def sauce_maker():
    sauce_dict = {"gochu": "spicy", 
                 "sesame": "fatty", 
                    "soy": "salty"}

    for sauce in sauce_dict:
        if sauce_dict[sauce] == "spicy":
            print(sauce)

But if I want to combine them, I need a different function that uses rand() and so on. This way I build up, eventually achieving my desired end goal. This process can exist within a 'top down' design, as well, for example one in which I know I want to add this piece to a larger program.

Flow Chart Design

Flow chart design can be top down or bottom up design.

Flow charts use series of block do desrcibe a program's control flow.

Here's an example. Feel free to use flow charts if you find it helps you plan your program.

Commenting Code

Creating comments

A large comment can be created with either of these two string terminators:

'''
"""

Using a large quote at the start of a file creates a module "doc-string" or "doc comment" which is like a global comment typically used to describe the entire program.

When you want a single line comment, you can use the pound sign, or hash tag:

#

Guidelines on comments

In a perfect world, your code would be self explanatory. That would be the "platonic ideal" code. However, we do not live in a perfect world. Some guidelines:

  • Do NOT comment obvious code
  • DO comment code that is not self-explanatory when you look at it the next day
  • Avoid inline comments where possible. Use some whitespace and add a comment above the code.
    print("hi") # This comment is inline
    # This comment is directly above the code
    print("hi")
    
    def print_hello():
        """
        This is the "best" way to add a 
        comment: at the top of a code
        structure such as a function.
        """
        print("hi")
  • Comments should be complete sentences.
  • Read and trust comments, but verify that they are correct. Oftentimes the code changes but a comment does not.

    The best comments tell the reader things that they could NOT glean from the code. External influences on the code, other approaches that failed, high-level reasons why something was designed a certain way...

    You tell me: good or bad comments?

  • # Take last elment of list
    last_element = my_list[-1]
  • Bad! Comment is a direct explanation of the code. And has a typo.
  • input().join(my_list[::-1]) # thing mixes up user
  • Horrible! Comment is confusing and makes no sense. And is also inline.
  • # Add integers from user to list until 
    # sentinel value is typed
    n = input(">>")
    while n != DONE_STRING:
        int_list.append(int(n))
        n = input(">>")
  • It's an OK comment. It is just a summary but it summarizes enough to possibly be useful.
  • def padovan_seq(n):
        """
        Starting values for this sequence,
        as defined in OEIS (oeis.org/A000931),
        F(0) == 1, and F(1) == F(2) == 0.
        """
        if n == 0:
            return 1
        if n <= 2:
            return 0
        return padovan_seq(n-3) + padovan_seq(n-2)
  • Very good comment! It helps explain an otherwise confusing part of the base case for this function.
  • Modularity and Testing

    Functions, classes, and modules

    These are all reusable pieces of code. We are only allowed to use functions, but classes and modules work similarly.

    Testing

    human outlines growing older in stages https://img.freepik.com/premium-vector/person-age-life-cycles-line-icon-human-growing-up-aging-child-adult-old-senior-person_352905-1603.jpg

    Testing begins bottom up. We should test smaller pieces of code, like our modular functions from above, and them build up into testing all code together.

    How would you test this function?

    def categorize_by_age(age):
        if 1 <= age <= 9:
            return "Child"
        elif 9 < age <= 18:
            return "Adolescent"
        elif 18 < age <= 65:
            return "Adult"
        elif 65 < age <= 150:
            return "Golden age"
        else:
            return "Invalid age: " + str(age)
    Sample starter test
    
    		def test_age():
    		"""
    		:return: True if all tests are passed, otherwise returns False
    		"""
    		tests_passed = True
    		if categorize_by_age(5) != "Child":
    			return False
    		elif categorize_by_age(10) != "Adolescent":
    			return False
    		# and so forth...
    		return True

    Edge cases

    An edge case is a test case or situation of parameters or input to a function or program which will force our code to do a calculation or process that is on the boundary of what we expect for a use case.

    If we are checking primes for instance, and allow integer input, 0, 1. Negatives may be invalid input or edge cases.

    Null (empty) lists, empty strings, strings of all whitespace (when we use strip) and other kinds of test cases like this are often considered edge cases as well.

    Are there any edge cases in the categorize_by_age function?

    What about age 0? That's valid (children between 0 and 6 months). Perhaps we could add something like:

    if categorize_by_age(0) != "Child":
        return False

    Debugging example

    Let's take an example piece of code from a prior day. It should ask for values from the user until they get all of the places in the DMV. Then, it needs to print all the correct values input. What's wrong with it?

    places_list = ["dc","virginia","maryland"]
    correct_list = []
    
    while len(places_list) > 0:
        input("Give place name: ").lower()
        user_input.split(",")
        for entry in user_list:
            if entry:
                print("Correct!", entry)
                places_list.remove(entry)
                correct_list.add(entry)
    
    print("Cool! Your correct values were:", correct_list)

    We can run it with Python, but also run it on our mental interpreter :) What are the syntax and logic errors?

    When in doubt, you can always use the verifiable print debugging! (Adding print statements to various lines to see what the state of variables is at that location.) Simple but it works very well!

    "Solution"
    places_list = ["dc","virginia","maryland"]
    correct_list = []
    
    while len(places_list) > 0:
        user_input = input("Give place name: ").lower()
        user_list = user_input.split(",")
        for entry in user_list:
            if entry in places_list:
                print("Correct!", entry)
                places_list.remove(entry)
                correct_list.append(entry)
    
    print("Cool!", correct_list)

    Debugging recursion example

    Let's say we were trying to do this problem:

    Challenge: Use recursion to output the padovan sequence (the same problem from HW4, https://oeis.org/A000931). It's fibonacci but with the second and third previous number rather than the first and second.

    Let's say this was my attempt. What's wrong with it? How can I find out? Are there any methods I can follow or techniques I can use?

    def padovan_seq(n):
        if n <= 2:
            return 0
        return padovan_seq(n-1) + padovan_seq(n-2)
    "Solution"
    def padovan_seq(n):
        # Base case
        if n == 0:
            return 1
        if n <= 2:
            return 0
        return padovan_seq(n-3) + padovan_seq(n-2)
    
    # Driver code
    if __name__ == "__main__":
        for x in range(20):
            print(padovan_seq(x))