class AndExercise(ExerciseStep): """ Let's practice now. Previously we wrote a function `is_valid_percentage` using `or`. Here's an example solution: __copyable__ def is_valid_percentage(x): if x < 0 or x > 100: return False else: return True assert_equal(is_valid_percentage(-1), False) assert_equal(is_valid_percentage(0), True) assert_equal(is_valid_percentage(50), True) assert_equal(is_valid_percentage(100), True) assert_equal(is_valid_percentage(101), False) Rewrite this function using `and` instead. """ hints = """ If you have something like `x < 0 and x > 100`, you're on the wrong track. That's going to be `False` for *any* value of `x`! The solution with `and` is different in several ways from the solution with `or`. Our solution with `or` first determines if `x` is an invalid percentage, else concludes validity. Using `and` will do this in reverse. You will have to reverse the `return` statements accordingly. You will have to change the comparison operators too. """ disallowed = [ Disallowed(ast.Or, label="`or`"), Disallowed(ast.In, label="`in`"), Disallowed(ast.If, label="`if`", max_count=1), ] def solution(self): def is_valid_percentage(x: int): if 0 <= x and x <= 100: return True else: return False return is_valid_percentage tests = { -1: False, 0: True, 50: True, 100: True, 101: False, }
class spongebob(ExerciseStep): """ Excellent!!! One more exercise, and then you can relax. Write a program which prints `sentence` mockingly, e.g: OnE MoRe eXeRcIsE, aNd tHeN YoU CaN ReLaX. Every second character should be lowercased, the rest should be uppercase. """ hints = """ This is similar to the previous exercise. The difference is when and where you set the condition variable. You will need to have a boolean variable which changes with every iteration. First write a small program which takes a boolean variable and flips it, i.e. if the variable is `True` it becomes `False` and if it starts out `False` it's changed to `True`. No loops, just an `if/else`. You will need to use the variable in the `if` condition and also assign to the same variable in the body. Combine that flipping `if/else` with the one that chooses an uppercase or lowercase character. """ parsons_solution = True def solution(self, sentence: str): upper = True new_sentence = '' for char in sentence: if upper: char = char.upper() upper = False else: char = char.lower() upper = True new_sentence += char print(new_sentence) tests = { 'One more exercise, and then you can relax.': 'OnE MoRe eXeRcIsE, aNd tHeN YoU CaN ReLaX.', } disallowed = [ Disallowed(ast.For, max_count=1, label="`for`"), Disallowed(ast.If, max_count=1, label="`if/else`"), ]
class capitalise(ExerciseStep): """ Time for a challenge! Write a program which, given a string `sentence`, prints a modified version with the same letters, where the first letter is capitalised and the rest are lowercase. For example, the output should be `Hello world` whether the input `sentence = 'hello world'` or `'HELLO WORLD'`. """ hints = """ You've learned all the tools you need for this. I believe in you! Look at previous programs for inspiration. You will need a loop to build up the new sentence character by character. You will need an `if/else` to choose whether to add an uppercase or lowercase character. Your `if/else` needs to execute different bodies depending on which iteration of the loop it's in. That means that your `if` condition needs to be a variable that changes inside the loop. In the first iteration you need an uppercase letter. In the following iterations you need a lowercase letter. """ parsons_solution = True def solution(self, sentence: str): upper = True new_sentence = '' for char in sentence: if upper: char = char.upper() else: char = char.lower() new_sentence += char upper = False print(new_sentence) tests = { 'HELLO THERE': 'Hello there', 'goodbye': 'Goodbye', } disallowed = [ Disallowed(ast.For, max_count=1, label="`for`"), Disallowed(ast.If, max_count=1, label="`if/else`"), ]
class upside_down_triangle_exercise(ExerciseStep): """ Wow, you're basically a hacker now! One more exercise. Given a size: size = 5 Print out an 'upside down' triangle made of the letter `O` whose sides are as long as the given size, e.g: OOOOO OOOO OOO OO O """ hints = """ How would you describe instructions to type in this triangle manually? Print a line of `size` Os, then `size - 1` Os, etc. down to 1 O. For example print 5 Os, then 4 Os, then 3, 2, and 1. Break this down into subproblems. How do you print one line of Os of a given length, and how do you go through all the lengths? Building up a line of characters should be very familiar from previous exercises, the only difference is that you have to make it a given length instead of just the same length as another string. An easy way to do something `n` times is to loop over `range(n)`. You need to use a for loop inside a for loop. You need numbers that count down, like 5, 4, 3, 2, 1. There is a way to do this with `range`, and you can easily look it up, but it's also easy to use a normal range and do some very simple maths to convert numbers counting up into numbers counting down. What formula converts 0 into 5, 1 into 4, 2, into 3, etc? """ parsons_solution = True def solution(self, size: int): for i in range(size): length = size - i line = '' for _ in range(length): line += 'O' print(line) disallowed = Disallowed(ast.Mult, label="`*`") tests = {3: """\ OOO OO O """, 5: """\ OOOOO OOOO OOO OO O """}
class fix_broken_program(ExerciseStep): """ As you can see we can define an f-string using double quotes too, like we can a normal string. And like quotes, f-strings are just notation. Once they are evaluated the computer forgets that an f-string was used, it just stores the final result as a normal string. Here is a very broken program: __copyable__ people = ["Alice", "Bob", "Charlie"] print('There are' + people.length() + 'people waiting, the first one's name is' + people.1 + '.') Fix it! Your solution should work for any list of strings named `people`. For example, in the above case it should print: There are 3 people waiting, the first one's name is Alice. """ hints = """ There are four problems with the expression inside `print`. There is a problem with the syntax that finds the number of people. Then one of the strings has a problem with the quotes. Also there is a problem with the syntax that finds the first person's name. And you can't add strings and numbers together! Did you properly use curly brackets in your f-string? """ disallowed = Disallowed(ast.Add, label="`+`") # TODO message: catch forgetting the f def solution(self, people: List[str]): print( f"There are {len(people)} people waiting, the first one's name is {people[0]}." ) tests = [ (["Alice", "Bob", "Charlie"], "There are 3 people waiting, the first one's name is Alice."), (["Dan", "Evelyn", "Frank", "George"], "There are 4 people waiting, the first one's name is Dan."), ]
class name_box_2(ExerciseStep): """ You're getting good at this! Looks like you need more of a challenge...maybe instead of putting a name in a box, the name should be the box? Write a program that outputs this: +World+ W W o o r r l l d d +World+ """ hints = """ You will need two separate for loops over `name`. Each line except for the first and last has the same characters in the middle. That means you can reuse something. Create a variable containing the spaces in the middle and use it many times. Use one loop to create a bunch of spaces, and a second loop to print a bunch of lines using the previously created spaces. """ parsons_solution = True def solution(self, name: str): line = '+' + name + '+' spaces = '' for _ in name: spaces += ' ' print(line) for char in name: print(char + spaces + char) print(line) tests = { "World": """\ +World+ W W o o r r l l d d +World+ """, "Bob": """\ +Bob+ B B o o b b +Bob+ """, } disallowed = [ Disallowed(ast.For, predicate=lambda outer: search_ast(outer, ast.For), message=""" Well done, this solution is correct! And you used a nested loop (a loop inside a loop) which we haven't even covered yet! However, in this case a nested loop is inefficient. You can make a variable containing spaces and reuse that in each line. """), ]
class name_box(ExerciseStep): """ Fantastic! By the way, when you don't need to use a variable, it's common convention to name that variable `_` (underscore), e.g. `for _ in name:`. This doesn't change how the program runs, but it's helpful to readers. Let's make this fancier. Extend your program to draw a box around the name, like this: +-------+ | World | +-------+ Note that there is a space between the name and the pipes (`|`). """ hints = [ "You did all the hard stuff in the previous exercise. Now it's just some simple string concatenation.", "You only need one for loop - the one used to make the line of dashes from the previous exercise.", "Don't try and do everything at once. Break the problem up into smaller, easier subproblems.", dedent("""\ Try writing a program that outputs: ----- World ----- """), "Since you need to output three separate lines of text, you will need to call `print()` three times.", dedent("""\ Try writing a program that outputs: | World | """), dedent("""\ Try writing a program that outputs: +-----+ |World| +-----+ (i.e. no spaces around `World`) """), ] parsons_solution = True def solution(self, name: str): line = '' for _ in name: line += '-' line = '+-' + line + '-+' print(line) print('| ' + name + ' |') print(line) tests = { "World": """\ +-------+ | World | +-------+ """, "Bob": """\ +-----+ | Bob | +-----+ """, } class missing_spaces(ExerciseStep, MessageStep): """ You're almost there! Just add a few more characters to your strings. Your loop is perfect. """ def solution(self, name: str): line = '' for _ in name: line += '-' line = '+' + line + '+' print(line) print('|' + name + '|') print(line) tests = { "World": """\ +-----+ |World| +-----+ """, "Bob": """\ +---+ |Bob| +---+ """, } disallowed = [ Disallowed( ast.For, max_count=1, message=""" Well done, this solution is correct! However, it can be improved. You only need to use one loop - using more is inefficient. You can reuse the variable containing the line of `-` and `+`. """, ) ]
class NotPriority(ExerciseStep): """ You can see in Bird's Eye that not True or True is interpreted by Python as (not True) or True rather than: not (True or True) So, `not` has higher priority than `or` if there are no parentheses. It's the same as how -1 + 2 means: (-1) + 2 rather than -(1 + 2) `not` also has higher priority than `and`. Again, the main thing to remember is to use parentheses or extra variables when in doubt. Exercise: Suppose you're writing a program which processes images. Only certain types of file can be processed. If the user gives you a file that can't be processed, you want to show an error: if invalid_image(filename): print("I can't process " + filename) Suppose that .png and .jpg files cannot be processed, but other file types can. Here's an example function to do that: __copyable__ def invalid_image(filename): if filename.endswith(".png") or filename.endswith(".jpg"): return False else: return True assert_equal(invalid_image("dog.png"), False) assert_equal(invalid_image("cat.jpg"), False) assert_equal(invalid_image("invoice.pdf"), True) This is longer than it needs to be. Rewrite `invalid_image` so that the body is a single line `return <expression>`, i.e. no `if` statement. It should pass the same tests. """ hints = [ dedent(""" What if you were instead asked to simplify this related but opposite function? def valid_image(filename): if filename.endswith(".png") or filename.endswith(".jpg"): return True else: return False assert_equal(valid_image("dog.png"), True) assert_equal(valid_image("cat.jpg"), True) assert_equal(valid_image("invoice.pdf"), False) """), "In that case there is a standard simplification trick you can apply that we discussed a few pages ago.", 'In particular the `returns` are redundant because `filename.endswith(".png") or filename.endswith(".jpg")` ' 'is already the desired boolean.', dedent(""" So you can just write: def valid_image(filename): return filename.endswith(".png") or filename.endswith(".jpg") """), "For the real exercise, you can do something similar.", "The difference in the real exercise is that the result is reversed.", "That is, `invalid_image` returns `True` when `valid_image` returns `False` and vice versa.", "Remember what `not` does?", ] disallowed = [ Disallowed(ast.If, label="`if`"), ] def solution(self): def invalid_image(filename: str): return not (filename.endswith(".png") or filename.endswith(".jpg")) return invalid_image tests = { "dog.png": False, "cat.jpg": False, "invoice.pdf": True, } @classmethod def generate_inputs(cls): result = generate_string() if random.random() < 0.5: result += random.choice([".png", ".jpg"]) return {"filename": result}
class AndHasHigherPriority(ExerciseStep): """ If you read it casually from left to right, you may think that: True or False and False is equivalent to (True or False) and False but it's actually equivalent to True or (False and False) This is because `and` has a higher priority than `or`. This is important because the first interpretation reduces to `True and False` which is `False`, while the second interpretation reduces to `True or False` which is `True`! You can try both options with parentheses in the shell to confirm. **The lesson here is to be extra careful when combining operators.** Either add parentheses to be safe or break up your expression into smaller parts and assign each part to a variable. This will make your code clear, readable, and unambiguous, and will save you from painful mistakes. Time for an exercise. Suppose you're writing a program to play tic-tac-toe, also known as noughts and crosses or Xs and Os. If you've never heard of tic-tac-toe, you can read the rules and play a few games [here](https://gametable.org/games/tic-tac-toe/). We need to check if someone has won a game. Our function `all_equal` is already helpful for checking rows. Write a function to check if someone has won a game by placing 3 of the same pieces on one of the diagonal lines. The board is given as 3 lists of 3 strings each, and the function should return a boolean. __copyable__ def diagonal_winner(row1, row2, row3): ... assert_equal( diagonal_winner( ['X', 'O', 'X'], ['X', 'X', 'O'], ['O', 'O', 'X'] ), True ) assert_equal( diagonal_winner( ['X', 'X', 'O'], ['X', 'O', 'O'], ['O', 'X', 'X'] ), True ) assert_equal( diagonal_winner( ['O', 'X', 'O'], ['X', 'X', 'X'], ['O', 'O', 'X'] ), False ) """ hints = """ How many diagonals are there on the board? Which entries of `row1, row2, row3` make up each diagonal? Every list always has 3 entries, so no need for a loop. There are two problems to solve here: checking for a win in a specific diagonal, and combining the checks for each diagonal. One problem can be solved using `and`, the other using `or`. There's a lot of similarity with the `all_equal` function. You can even call that function to help! But then you have to include its definition. Similar to `all_equal`, check that the 3 entries on a diagonal are equal to each other, e.g. by using `and`. Check the two diagonals together, using `or`. """ disallowed = [ Disallowed(ast.If, label="`if`"), ] def solution(self): def diagonal_winner(row1: List[str], row2: List[str], row3: List[str]): middle = row2[1] return ( (middle == row1[0] and middle == row3[2]) or (middle == row1[2] and middle == row3[0]) ) return diagonal_winner @classmethod def generate_inputs(cls): return { "row1": [random.choice(["X", "O"]) for _ in range(3)], "row2": [random.choice(["X", "O"]) for _ in range(3)], "row3": [random.choice(["X", "O"]) for _ in range(3)] } tests = [ ((["X", "O", "X"], ["X", "X", "O"], ["O", "O", "X"]), True), ((["X", "O", "O"], ["X", "O", "X"], ["O", "X", "X"]), True), ((["X", "O", "X"], ["X", "O", "X"], ["O", "O", "X"]), False), ]
class AnExercise(ExerciseStep): """ When we inspect it with Bird's Eye, we can see that: name == "Alice" or "Bob" is not translated into name == ("Alice" or "Bob") the way we think in English, but rather: (name == "Alice") or ("Bob") which evaluates to `"Bob"` when `name == "Alice"` is `False`. Perhaps you feel like this: [![I now have additional questions](https://i.imgur.com/jN57tGt.png)](https://imgur.com/a/icKzI) The only thing you really need to know is this: Until you know what you're doing, always make sure you put booleans on both sides of `or`, because it's a boolean operator. `name == "Alice" or "Bob"` breaks that rule. If you're curious, the answers are below, but you can skip them if you want and move onto the exercise below. ---- > Why does `(name == "Alice") or ("Bob")` equal `"Bob"`? Why does it equal anything? `"Bob"` isn't even a boolean! The definition "`A or B` is `True` if either `A` or `B` is `True`" was a simplification. It's the easiest way to think about `or` most of the time, especially for writing `if` statements. The real definition is that if `A` is true then `A or B` is just `A` (in fact `B` is not even evaluated), otherwise it's `B`. You can see for yourself that if `A` and `B` are booleans then the two definitions are equivalent. In this example `A` is `name == "Alice"` which is `False`, so `A or B` is `B` which is `"Bob"`. > Is there a better way to write the condition without repeating `name ==` each time? Yes! In [Functions and Methods for Lists](/course/?page=FunctionsAndMethodsForLists) we mentioned the `in` operator, which you can use with a list like this: return name in ["Alice", "Bob", "Charlie"] But you can't always get rid of `or` like that. ---- Exercise: Write a function named `is_valid_percentage`, accepting one numerical argument `x`. It should return `True` if `x` is between 0 and 100 (inclusive), and return `False` otherwise. Your function should use `or`, and pass these tests: __copyable__ assert_equal(is_valid_percentage(-1), False) assert_equal(is_valid_percentage(0), True) assert_equal(is_valid_percentage(50), True) assert_equal(is_valid_percentage(100), True) assert_equal(is_valid_percentage(101), False) """ hints = """ Remember, you can use comparison operators `<, >, <=, >=, ==` to produce booleans. You need to check how `x` compares to 0 and how it compares to 100. You need to combine the two comparisons into one boolean using `or`. Above we used a trick so that the whole function body was just `return <comparison> or <comparison>`. But that won't work here! You need to use an `if` statement. You need to have a `return False` and a `return True`. If you have something like `x >= 0 or x <= 100`, you're on the wrong track. That's going to be true for *any* value of `x`. After all, 101 is greater than 0! """ disallowed = [ Disallowed(ast.And, label="`and`"), Disallowed(ast.In, label="`in`"), Disallowed(ast.If, label="`if`", max_count=1), Disallowed(ast.Compare, label="comparison chaining", predicate=lambda node: len(node.ops) > 1), ] # TODO catch wrong comparisons def solution(self): def is_valid_percentage(x: int): if x < 0 or x > 100: return False else: return True return is_valid_percentage tests = { -1: False, 0: True, 50: True, 100: True, 101: False, }
class winner(ExerciseStep): """ Bravo! That was quite tough. Now we can put the three functions together! Write a function `winner` that takes an argument `board` as before, and returns `True` if `board` contains either a winning row, column or diagonal, `False` otherwise. Your solution should work by calling the three functions. `winner` itself should not do any looping, subscripting, etc. Here is some code for `row_winner`, `column_winner` and `diagonal_winner`, along with some tests for `winner`. Click the Copy button, and fill in the blanks for your `winner` function. __copyable__ def winner(board): ... def winning_line(strings): piece = strings[0] if piece == ' ': return False for entry in strings: if piece != entry: return False return True def row_winner(board): for row in board: if winning_line(row): return True return False def column_winner(board): for col in range(len(board[0])): column = [] for row in board: column.append(row[col]) if winning_line(column): return True return False def diagonal_winner(board): diagonal1 = [] diagonal2 = [] for i in range(len(board)): diagonal1.append(board[i][i]) diagonal2.append(board[i][i]) return winning_line(diagonal1) or winning_line(diagonal2) assert_equal( winner( [ ['X', 'X', 'X', ' '], ['X', 'X', ' ', ' '], ['X', ' ', 'O', 'X'], [' ', ' ', 'O', 'X'] ] ), False ) assert_equal( winner( [ ['X', ' ', 'X'], ['O', 'X', 'O'], ['O', 'O', 'O'] ] ), True ) assert_equal( winner( [ ['X', ' '], ['X', 'O'] ] ), True ) """ hints = """ The solution is quite short! Simply use the three functions correctly. Think about possible cases. When does `winner(board)` return `False`? When does it return `True`? How can you use the three functions and a boolean operator together to get the result you need? """ disallowed = Disallowed((ast.For, ast.Subscript), function_only=True, message=""" Your solution should work by calling the three functions. `winner` itself should not do any looping, subscripting, etc. It should be very short. Copy the `row_winner` and other functions and leave them as they are. Don't copy code from them into the `winner` function, just call those functions. """) def solution(self): def winning_line(strings): piece = strings[0] if piece == ' ': return False for entry in strings: if piece != entry: return False return True def row_winner(board): for row in board: if winning_line(row): return True return False def column_winner(board): for col in range(len(board[0])): column = [] for row in board: column.append(row[col]) if winning_line(column): return True return False def diagonal_winner(board): diagonal1 = [] diagonal2 = [] for i in range(len(board)): diagonal1.append(board[i][i]) diagonal2.append(board[i][i]) return winning_line(diagonal1) or winning_line(diagonal2) def winner(board: List[List[str]]): return row_winner(board) or column_winner( board) or diagonal_winner(board) return winner @classmethod def generate_inputs(cls): return {"board": generate_board(choice(['row', 'col', 'diag']))} tests = [ ([[" ", "A", "B"], [" ", "A", "B"], [" ", "B", "A"]], False), ([[" ", "A", "A"], [" ", "A", "B"], ["B", "B", "B"]], True), ([["S", "M", " ", "M"], ["S", "M", "S", " "], ["S", "S", "M", "S"], ["S", "M", " ", "S"]], True), ([["O", "O", " ", "X", "X"], ["X", "O", "O", "X", "X"], ["X", "O", "O", "O", " "], ["X", "O", "O", "O", " "], ["O", "X", " ", "O", "O"]], True), ]
class fixing_type_errors_with_conversion(ExerciseStep): """ Using a string instead of an integer in `range` like `range('5')`, or in list subscripting like `list['3']` will also lead to an error. Most of these problems can be solved by converting the string to an integer by using `int` as a function: `int('5')` will return the integer `5`. Similarly an integer can be converted to a string by using `str` as a function: `str(5)` will return the string `'5'`. Using this new knowledge, fix this broken program: __copyable__ number = '3' for i in range(number): print('Starting... ' + i + 1) print('Go!') The correct program should print: Starting... 1 Starting... 2 Starting... 3 Go! Your solution should work for any value of the variable `number`. """ hints = """ At what points is this code broken? There are values that need to be converted to a different type. Specifically there's a `str` that needs to be converted to an `int`. And an `int` that needs to be converted to a `str`. """ tests = [ ('1', """\ Starting... 1 Go! """), ('2', """\ Starting... 1 Starting... 2 Go! """), ('3', """\ Starting... 1 Starting... 2 Starting... 3 Go! """), ] disallowed = Disallowed(ast.JoinedStr, label="f-strings") def solution(self, number: str): for i in range(int(number)): print('Starting... ' + str(i + 1)) print('Go!') @classmethod def generate_inputs(cls): return {"number": str(randint(1, 10))}