Day 15: Debugging
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
If we know what the end goal of our program is, we can break it up into small parts.
- The top of this design is our overall vision. We can see what we want it to become, like we are looking at our project from above.
- We then drill down into specific functions of our program, and break those apart into workable chunks.
- These workable chunks must satisify requirements and constraints in order to work with other chunks and achieve the program's end goal.
- If we have a list of all requirements, that is called having a specification.
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
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.
- The bottom of this design is our first thought about what should be worked on.
- We then build up our ideas into workable chunks.
- These workable chunks must still satisify a program's specifications (assuming you have one).
- gochujang
- sesame oil
- soy sauce
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:
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")
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]
input().join(my_list[::-1]) # thing mixes up user
# Add integers from user to list until
# sentinel value is typed
n = input(">>")
while n != DONE_STRING:
int_list.append(int(n))
n = input(">>")
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)
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.
- Functions separate our code.
- We can use a function as many times as we like.
- Functions make testing easier, because we can print errors within a function and know where our program went wrong.
- We can write tests for our functions to ensure they work properly.
- Multiple programmers can write functions at the same time to contribute to the same project.
Testing
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.
- Tests should be concise and test for one specific thing at a time (negative input, wrong data types, etc)
- A single function should therefore have multiple tests, each one testing for something different
- Tests that pass should indicate the function is behaving as expected
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))