--- tags: Labs-F20, 2020 title: Lab 09 --- # Lab 9: Python Practice Let's get some more practice with our new favorite programming language, Python! ## Learning Objectives Our goal for lab this week is to get you comfortable with Python syntax, for loops, debugging, and Python's built-in function for sorting lists. If you don't feel confident about any of these topics after your lab session, definitely come to TA Hours! We'd love to help you. ## Part 0 - Installing PyCharm If you haven't yet installed PyCharm, the editor we'll be using to write our Python programs, let's get set up with that now! Follow the [PyCharm Installation and Project Setup](https://cs.brown.edu/courses/csci0111/spring2020/docs/pycharm_setup.html) guide! If you get stuck or run into problems at any step, call over a TA! When writing your programs in PyCharm, you have a choice: you can either create a new project in PyCharm for ALL your files, or create a new project for each assignment. Just make sure to include `testlight.py` into each project you create. ## Part 1: Debugging ### Debugging in PyCharm Consider the following function, `add_underscores(word: str) -> str`, which takes in a word and returns that same word, but with underscores added before and after each letter in the word (i.e. `"hello"` -> `"_h_e_l_l_o_"`): ``` def add_underscores(word): """adds underscores before & after each letter in word""" new_word = "_" for i in range(len(word)): new_word = word[i] + "_" return new_word phrase = "hello" print(add_underscores(phrase)) # should print out "_h_e_l_l_o_" ``` Copy and paste this snippet of code into a Python file, and run the program. You should see this output: ``` o_ ``` Uh oh! We were expecting to get `_h_e_l_l_o_`, so this means our program has a bug! Let's use the PyCharm Debugger to find it. Right click your code, and click on the prompt that says "Debug lab9": ![right click menu in PyCharm](https://i.imgur.com/DAHtJ8k.png) After that, the debugger should open at the bottom of your screen. The area where our bug must be occurring is at the line that says `new_word = word[i] + "_"`, since that's where we're supposed to be adding underscores but are currently failing to do so properly. To investigate this line of code in particular, we're going to use a **breakpoint**, which will stop our code at a certain line so we can see values of variables and how they change at a given moment in time. To add a breakpoint, click on the line number to the left of your code. ![clicking the line number in PyCharm](https://imgur.com/oEH6RzG.gif) Now, right click again and click "Debug lab9." At the bottom of your screen, you should see something like this: ![debugger frame 1](https://i.imgur.com/2uEkQoc.png) Now we can see the values of `i`, `new_word`, and `word` at the moment in time when our program reaches that line where we set the breakpoint! There are a few important buttons here: - In the leftmost column: - The green arrow button towards the top will rerun your program in the debugger. - **The green play button** will "unpause" your code -- if it hits the breakpoint again, it'll stop again, and otherwise it will stop after your program finishes execution. - The red square button stops running your code in the debugger. - In the row of buttons above "Variables": - The button that displays just a **blue arrow pointing down** ("Step Into") will allow you to move to the next line being executed by your program, whether or not that's a breakpoint. Play around with the green play button and the "Step Into" button. Can you find the bug? ___ ### *CHECKPOINT:* Call over a TA once you reach this point. ___ ### Debugging with `print` While PyCharm's debugger can be useful, sometimes we want a quicker, less rigorous option for debugging. Let's look at another program: ``` def mystery_fun(num_list: list) -> int: return_value = 0 for num in num_list: if num_list[num] % 2 == 0: return_value += 1 if num_list[num] > 5: return_value -= 1 if num_list[num] > 1: return_value += num return return_value input = [2, 0, 7, 5, 1, 8, 4, 3, 5] print(mystery_fun(input)) ``` Copy and paste this piece of code into PyCharm and run the program. What does it print? The body of our function is a bit confusing -- let's try to figure out what it's doing. While the PyCharm debugger could be used here, it might not immediately be instructive as to what the function is actually doing. To see what our function is actually doing, let's see how we can place `print` statements within our function to help us decipher what is changing and when. Add this `print` statement within your for loop, above the if-statements: ``` print(num_list[num]) ``` Now run your program -- before it prints out the result of calling `mystery_fun(input)`, it should print a bunch of numbers that correspond to `num_list[num]` at each iteration of the loop. Can you see more clearly how `return_value` changes throughout this function? Describe how `return_value` is modified as it progresses through each number in `input`. ***HINT:*** If you're not sure yet, try adding other `print` statements to make it more clear what's going on. ___ ### *CHECKPOINT:* Call over a TA once you reach this point. ___ ## Part 2: Maps and Filters in Python The [Cake Boss](https://en.wikipedia.org/wiki/Cake_Boss) himself, Buddy Valastro, has decided to open up a bakery on Thayer Street! To better understand the preferences of Brown students, he decides that the best plan of action is to pretend to be a Brown student and figure out their preferences from the _inside_. For Buddy to believably pass as a Brown student, he must first enroll in some classes. Here is a list of the courses that he can enroll in this semester: ``` course_list = ["CSCI0180", "CSCI0160", "CSCI0220", "CSCI0320", "CSCI1420", "CSCI1951", "APMA0330", "APMA0350", "APMA1650", "ENGL0220", "ENGL0510", "CLPS0150", "CLPS0450", "CLPS0900", "CLPS1360"] ``` --- :::spoiler **String Slicing** (click to expand) :::info **NOTE:** To do many of the following problems, you will need to do something called *string slicing*. String slicing is the Python equivalent of Pyret's `string-substring`, but winds up being a lot more powerful.<br> Let's say you have the string `str = "Beep!"`. Just like in Pyret, strings in Python are **0-indexed:** - The "B" is at **index 0** - The "e"s are at **indices 1 and 2** - The "p" is at **index 3** - The "!" is at **index 4** To access the characters at those indices, use the notation: `str[index]`. For example, ``` >>> str[0] "B" >>>str[4] "!" ``` If you want to slice a string into a substring of *more than one character*, use the notation: `str[start:end]` where `start` represents the first index of the substring (inclusive) and `end` represents the last index of the substring (exclusive). For example, ``` >>> str[0:5] "Beep!" >>> str[1:4] "eep" >>> str[2:3] # note that this is equivalent to str[2] "e" >>> str[2:2] # no characters in range, so this outputs the empty string "" ``` That's all you need to know for the lab, but Python has a few additional shortcuts for string slicing that might be useful in the long run: - If you **leave off the start or end index**, Python assumes you want to start at 0 and end at the end of the string: ``` >>> str[:3] # equivalent to str[0:3] "Bee" >>> str[3:] # equivalent to str[3:5] "p!" >>> str[:] # equivalent to str[0:5] "Beep!" ``` - **Negative numbers** let you index from the end of a string ``` >>> str[-1] # equivalent to str[4] "!" >>> str[-5] # equivalent to str[0] "B" >>> str[-3:-1] # equivalent to str[2:4] "ep" ``` ::: --- Buddy got over-excited and registered for so many courses that he can't keep track of them anymore! Help him sort through and organize his courses using Python: 1. Write a function `dept_courses(dept: str)` that takes in a 4-letter department code and produces a list of all courses in department `dept`. Then, write a function `course_num(num: str)` that takes in a 4-digit course number and produces a list of all courses with that number (regardless of department). 2. Write a function `course_search(dept: str, min: int, max: int)` that takes in a 4-letter department code, a minimum course code, and a maximum course code, and produces a list of all courses in department `dept` with numbers between `min` and `max`, inclusive. 3. Write a function `pretty_print(dept: str)` that takes in a 4-letter department code and prints out the full name of a department followed by all of its courses. Each course should be on a separate line with "-" before the course name. For example: ``` >>> pretty_print("CSCI") Computer Science - CSCI0180 - CSCI0160 - CSCI1420 - CSCI1951 ``` 3. Write a function `compare_depts(dept1: str, dept2: str)` that takes in the 4-letter department codes of two departments and returns the 4-letter department code of the department that is offering more courses. If both departments are offering the same number of courses, the function should return "equal" (`str`). If a department isn't offering any courses, raise an exception. ___ ### *CHECKPOINT:* Call over a TA once you reach this point. ___ Buddy has a couple of other tasks he needs to write some Python code for: 1. Write a function `obscure` that takes a string and replaces certain letters with numbers (a common, but ineffective, password protection technique). Specifically, convert every e (or E) to 3, every i (or I) to 1, and every o (or O) to zero. Write a helper function that takes in a single character and converts it if necessary (and keeps it the same if not). :::warning **NOTE:** You can use == to compare strings, or check whether a character is in a list (such as ["a", "A"]) of characters to be treated similarly. To check whether an item is in a list, use an `in` expression of the form `item in list`. ::: 2. Write a function `sorted_increasing` that determines whether a list of numbers is sorted into increasing order. :::warning **NOTE:** The Boolean type is written as `bool` in Python; `True` and `False` (with first letter capital) are the two boolean values. ::: :::info **HINT:** The challenge here is to figure out how to track the previous number in the list. Think about how a variable could help you do that. ::: 3. Write a function `first_five_pos` that takes a list and returns a list of the first five positive numbers from the input list. ___ ### *CHECKPOINT:* Call over a TA once you reach this point. ___ ## Part 3: Sorting Lists in Python :::spoiler **Basic Sorting** (click to expand) Python has a useful and customizable sorting function that operates on lists. In this section, we will explore how to use it. The simplest version of the function is `sorted(lst: list)`, which takes in a list and sorts it in ascending order. - Lists of integers are sorted numerically - Lists of strings are sorted alphabetically - Lists of booleans are sorted with all Falses before all Trues For example: ``` >>> sorted([1,5,3,1]) [1,1,3,5] >>> sorted(["bc", "abc", "d", "aa", "aaa"]) ['aa', 'aaa', 'abc', 'bc', 'd'] >>> sorted([False, True, True, False, True]) [False, False, True, True, True] ``` To sort a list in descending order, add the input `reverse=True` to the function call. Here are the examples from above but sorted in reverse: ``` >>> sorted([1,5,3,1], reverse=True) [5,3,1,1] >>> sorted(["bc", "abc", "d", "aa", "aaa"], reverse=True) ['d', 'bc', 'abc', 'aaa', 'aa'] >>> sorted([False, True, True, False, True], reverse=True) [True, True, True, False, False] ``` :::warning **NOTE:** Notation like `reverse=True` is used for optional inputs to a function, a concept we did not see in Pyret. Since an optional input might not be provided, we need the `<name>=` part to indicate which optional parameter is being used. ::: --- :::spoiler **Custom Sorting** (click to expand) In most cases, default sorting is enough. However, what if you want to sort a list in a specific, custom way? To do so, add the input `key=my_fun` where `my_fun` represents a function that takes in a single list element. `my_fun` is called on each element in the list, and **its output** determines the sort order. Let's take a look at a concrete example. The following function takes in a string and returns its length: ``` def key_fun(elem: str): return len(elem) ``` With `key_fun` defined, we can do the following to sort the list by string length: ``` >>> sorted(["Buddy Valastro", "does", "love", "sorting", "lists"], key=key_fun) ['does', 'love', 'lists', 'sorting', 'Buddy Valastro'] ``` *(Note that since string length is an integer, `sorted` defaults to sorting the elements in increasing order)* ::: --- Let's apply the ideas of sorting discussed above to the course list examples from earlier: 1. Sort `course_list` in alphabetical order by department code. 2. Sort `course_list` by course number, regardless of department code. ___ ### *CHECKPOINT:* Call over a TA once you reach this point. ___ Buddy just signed up to compete in a Mystery Scrabble competition. While he is an excellent baker, he happens to be very bad at this game and needs your help to win. He wants to use custom sorting to efficiently find the best word. In this version of Scrabble, point values for words are based solely on the length of a word, rather than the letters themselves. Given a list of possible words, he wants to know which ones will secure them the most points -- so instead of sorting the list alphabetically, **he sorts the list based on the length of each word in it.** :::info Another way to think about what's happening is to imagine mapping the `key` function on the list, sorting, and then undoing the mapping. In other words: **Step 1 (the original list):** ["Buddy Valestro", "really", "loves", "sorting"] **Step 2 (the mapped list):** [14, 6, 5, 7] **Step 3 (the sorted order):** [5, 6, 7, 14] **Step 4 (the "un-mapped" list):** ["loves", "really", "sorting", "Buddy Valestro"] ::: :::warning **NOTE:** Note that if multiple elements mapped to the same thing -- in this case, multiple words had the same number of letters -- they would stay in their original order (this is called stable sorting). ::: Buddy has decided to up the ante by adding some new, more complex, rules to the game. Awarding a point value for a word has been adjusted to the following: * Vowels (including y): **1 point** * Consonants: **2 points** * Unless the consonant is z/q/x, in which case it is worth **5 points** 1. Write a function `sort-by-score(lst : list)` that takes in a list of strings and sorts them by their score (in descending order, i.e. highest to lowest) using the updated scoring system. For example, "beep" has a score of 2+1+1+2=6 and `sort-by-score(["aaa", "aba", "z", "beep", "BOOP"])` should return `["beep", "BOOP", "z", "aba", "aaa"]`. ___ ### *CHECKPOINT:* Call over a TA once you reach this point. ___ ## Board Game Night Champs Buddy had a blast at Board Game Night. Needless to say, Mystery Scrabble was a hit, and Buddy Valastro was declared the champion. He used the custom-sorting functions to win the game and figure out his courseload, and now he's ready to sell cakes to anyone who passes by his shop!