<style> .present { text-align: left; } img[alt=knight_moves] { width: 400px; } </style> # (Knight's Travail version) More Built-ins, Comprehensions, and Classes ## Week 17 Day 4 --- ## Part 1: More Built-ins --- ### Lecture Videos (22 minutes) Watch: - Built-Ins: All And Any (8:00) - Built-Ins: Filter and Map (10:32) --- ### Built-in functions: `all()` `all()` returns `True` if *all* items in a collection are truthy or if the iterable is empty. It returns `False` if there is at least one falsey item. ```python= test1 = {"item", "truthy", ""} test2 = [] test3 = [[]] print(all(test1)) print(all(test2)) print(all(test3)) ``` --- ### Built-in functions: `any()` `any()` returns `True` if there are *any* truthy items in the provided collection. It returns `False` if there are no truthy items or if the iterable is empty. ```python= test1 = ["item", [], []] test2 = [] test3 = [[]] print(any(test1)) print(any(test2)) print(any(test3)) ``` --- ### Built-in functions: `filter()` `filter()` takes in a function and an iterable as arguments and returns a *filter object*. The returned collection includes only the items which, when the function parameter was applied to them, returned a truthy value. `filter()` does not filter in place. It returns an entirely new object. ```python= def is_a(num): if num >= 90: return True else: return False scores = [90, 86, 75, 91, 62, 99, 88, 90] only_as = filter(is_a, scores) # does not mutate original print(only_as) # <filter object at 0x10546ad30> print(list(only_as)) # [90, 91, 99, 90] ``` --- ### Built-in functions: `filter()` `filter`'s function parameter can also be defined in line as a `lambda` function. ```python= scores = [90, 86, 75, 91, 62, 99, 88, 90] only_as = filter(lambda num: num >= 90, scores) print(only_as) # <filter object at 0x10546ad30> print(list(only_as)) # [90, 91, 99, 90] ``` --- ### Built-in functions: `map()` `map()` takes in a function and an iterable as arguments and returns a *map object*. `map()` transforms each value from the original iterable according to the provided function and returns them in a new object. ```python= def get_grade(num): if (num >= 90): return "A" elif (num <90 and num >= 80): return "B" elif (num < 80 and num >= 70): return "C" elif (num < 70 and num >= 60): return "D" else: return "F" scores = [90, 86, 75, 91, 62, 99, 88, 90] print(map(get_grade, scores)) # <map object at 0x106faffa0> grades = list(map(get_grade, scores)) print(grades) # ['A', 'B', 'C', 'A', 'D', 'A', 'B', 'A'] ``` --- ### Built-in functions: `zip()` `zip()` takes two iterables as arguments and returns a *zip object* that pairs values at corresponding indices. You can typecast the *zip object* as a sequence of tuples or as a dictionary. ```python= scores = [90, 86, 75, 91, 62, 99, 88, 90] grades = ["A", "B", "C", "A", "D", "A", "B", "A"] combined = zip(scores, grades) combined_list = list(combined) combined_dict = dict(combined_list) print(combined) # <zip object at 0x1023a9600> print(combined_list) # [(90, 'A'), (86, 'B'), (75, 'C'), (91, 'A'), (62, 'D'), (99, 'A'), (88, 'B'), (90, 'A')] print(combined_dict) # {90: 'A', 86: 'B', 75: 'C', 91: 'A', 62: 'D', 99: 'A', 88: 'B'} ``` --- ## Part 2: Comprehensions --- ### Lecture videos (22 minutes) Watch: - List Comprehensions Demo (18:50) --- ### Comprehensions Comprehensions are composed of an expression followed by a `for...in` statement, followed by an optional `if` clause. They can be used to create new lists (or other mutable sequence types). ```python= my_list = [expression for member in iterable] # with optional if statement my_list = [expression for member in iterable if condition] ``` --- ### Copying a list With a `for` loop: ```python= my_list = [1, "2", "three", True, None] my_list_copy = [] # for loop # ---------- # / \ for item in my_list: my_list_copy.append(item) # | # var print(my_list_copy) # [1, '2', 'three', True, None] ``` --- ### Copying a list With a list comprehension: ```python= my_list = [1, "2", "three", True, None] # var for loop # | ------------- # | / \ my_list_copy = [item for item in my_list] print(my_list_copy) # [1, '2', 'three', True, None] ``` --- ### Mapping over a list with comprehensions Include the desired expression before the `for` statement ```python= my_list = ["jerry", "MARY", "carrie", "larry"] # expression for loop # | ------------- # | / \ mapped_list = [item.lower() for item in my_list] print(mapped_list) # ['jerry', 'mary', 'carrie', 'larry'] ``` --- ### Convert `map()` to list comprehension ```python= nums = [-5, 11, 10, 14] mapped_nums = map(lambda num: num * 2 + 1, nums) print(list(mapped_nums)) # [-9, 23, 21, 29] ``` --- ### Convert `map()` to list comprehension Answer: ```python= nums = [-5, 11, 10, 14] # mapped_nums = map(lambda num: num * 2 +1, nums) mapped_nums = [num * 2 + 1 for num in nums] print(mapped_nums) # [-9, 23, 21, 29] ``` --- ### Filtering a list with comprehensions ```python= nums = [-5, 11, 10, 14] filtered_nums = filter(lambda num: num > 0, nums) print(list(filtered_nums)) # [11, 10, 14] ``` --- ### Filtering a list with comprehensions Answer: ```python= nums = [-5, 11, 10, 14] # filtered_nums = filter(lambda num: num > 0, nums) filtered_nums = [num for num in nums if num > 0] print(filtered_nums) # [11, 10, 14] ``` --- ### Nested loops Nested for loop: ```python= letters = ["a", "b", "c"] nums = [1, 2] new_list = [] # outer loop # ---------- # / \ for l in letters: for n in nums: # <- inner loop new_list.append((l, n)) # \ / # --- # expression print(new_list) # [('a', 1), ('a', 2), ('b', 1), ('b', 2), ('c', 1), ('c', 2)] ``` --- ### Nested loops (list comprehension) With list comprehension—note that the outer loop is first: ```python= letters = ["a", "b", "c"] nums = [1, 2] # expression outer loop inner loop # --- -------------- ----------- # / \ / \ / \ new_list = [(l, n) for l in letters for n in nums] print(new_list) # [('a', 1), ('a', 2), ('b', 1), ('b', 2), ('c', 1), ('c', 2)] ``` --- ### Dictionary comprehensions Use a colon in the expression to separate the key and value. ```python= # a dictionary that maps numbers to the square of the number number_dict = {num: num**2 for num in range(5)} print(number_dict) # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16} ``` --- ## Part 3: Classes --- ## Lecture videos (23 minutes) Watch: - Classes in Python Demo (19:18) --- ### Classes To create a class we use the `class` keyword, and by convention, we capitalize the names of classes. ```python= # python example class Icon: # more code to come ``` Looks a lot like JavaScript ```javascript= // javascript comparison class Icon { // more code to come } ``` --- ### Initializing classes Generally speaking: - Python's `__init__()` is like JavaScript's `constructor()` - Python's `self` is like JavaScript's `this` ```python= # python example class Icon: def __init__(self): # more code to follow ``` ```javascript= // javascript comparison class Icon { constructor() { // more code to follow } } ``` --- ### the `__init__()` method Python's constructor method is called `__init__()`. ```python= # python example class Icon: def __init__(self, color, shape): self.color = color self.shape = shape ``` ```javascript= // javascript comparison class Icon { constructor(color, shape) { this.color = color; this.shape = shape; } } ``` --- ### Instance variables and methods You can set attributes on the instance with dot notation (`self.some_attribute = value`). You can add methods to the class by defining functions and passing in `self`. ```python= class Icon: def __init__(self, color, shape): self.color = color self.shape = shape def my_method(self, word): print(f"hello {word}") return ``` --- ### Wait, what _is_ `self`? `self` refers to the instance that a method was called on. Whenever you invoke a method on an instance of a class, it is as though you are invoking the class's own method, and passing in the instance as an argument. ```python= some_icon = Icon("blue", "square") # both below do the same thing some_icon.my_method("other argument") Icon.my_method(some_icon, "other argument") ``` --- ### Wait, what _is_ `self`? Calling the first parameter `self` is a convention. This is technically valid Python, and it would do the same thing as calling it `self`. But no one would write it this way. ```python= class Icon: def __init__(banana, color, shape): banana.color = color banana.shape = shape ``` --- ### Instances of classes We create instances of a class by invoking the class as though it is a function (this invokes the class's `__init__()` method) ```python= # in python my_new_icon = Icon("blue", "circle") ``` ```javascript= // javascript comparison const myNewIcon = new Icon("blue", "circle"); ``` --- ### Inheritance To inherit from another class, we pass a reference to that class as an argument in the class definition. We can use the `super()` function to get a reference to the parent class, then invoke the desired function. ```python= class Icon: def __init__(self, color, shape): self.color = color self.shape = shape class Gadget(Icon): def __init__(self, color, shape, noise): super().__init__(color, shape) self.noise = noise thingie = Icon("purple", "spiral", "whrrrrrr") print(thingie) # <__main__.Gadget object at 0x105103d60> print(thingie.color, thingie.shape, thingie.noise) # purple spiral whrrrrrr ``` --- ### Getters and setters Getters & setters allow us to have methods that behave like properties. They provide a convenient interface for implementing more complicated logic necessary for setting a class property. They can also be useful for protecting "private" values on your class. --- ### Getters A getter allows you to define a method that behaves like a property. The `@property` decorator over a method creates a getter. While the getter is a function, it is invoked as if it were a property. ```python= class Icon(): def __init__(self, color, shape): self.color = color self.shape = shape @property def info(self): print("in the getter!") return f"{self.color} {self.shape}" my_icon = Icon("blue", "square") print(my_icon.color) # call the getter method as if we were just # accessing a property print(my_icon.info) ``` --- ### Setters A setter allows you to define a method that updates the getter "property". The decorator used to create a setter is `@<getter_method_name>.setter`. You can have a standalone getter, but you must have a getter in order to have a setter. The setter method runs when you change the getter "property." ```python= class Icon(): def __init__(self, color, shape, pswd): self.color = color self.shape = shape # set initial ~secret~ password # this calls the setter method! self.my_password = pswd @property def info(self): print("in the getter") return f"{self.color} {self.shape}" # getter for ~secret~ password @property def my_password(self): return self._password # setter for ~secret~ password @my_password.setter def my_password(self, new_val): print("hashing password....") self._password = str(new_val) + "12345" * 3 my_icon = Icon("blue", "square", "beepboop") print(my_icon.my_password) # call the setter method as if we were # setting my_password as a regular property my_icon.my_password = "new thing" print(my_icon.my_password) ``` --- ### "Private" properties Python does not really have private attributes. All properties on a class can be accessed from outside the class/instance. Getters and setters just help us indicate that certain properties should not be directly accessed in this way. Conventionally, private properties are indicated with a single underscore followed by the property name: `self._password` --- ### Duck-Typing "If it looks like a duck, and quacks like a duck, it must be a duck" - What does it mean for an object to "look/quack like a duck"? - it has the relevent "magic method" that python uses to implement a given built-in function (like `len()` or `print()`) - What does it mean to "be a duck"? - that means built-in functions will be able to work on classes that you create - Why is this good? - duck-typing means that classes that you define can work as though they are already part of python! --- ### Duck-typing Using duck-typing you can: - have your class work with the addition operator (`+`) - define what equality operator (`==`) means for your class - loop over an instance of your class with `for ... in` - and much more! - duck-typing is powerful! --- ### Duck-typing The default value when you print a user-defined class instance is not typically very helpful... ```python= class Icon: def __init__(self, color, shape): self.color = color self.shape = shape my_new_icon = Icon("blue", "circle") print(my_new_icon) # <__main__.Icon object at 0x1071cebb0> ``` --- ### Duck-typing You can use the `__repr__` method that will let you control what gets printed for your class. ```python= class Icon: def __init__(self, color, shape): self.color = color self.shape = shape def __repr__(self): return f"Icon({self.color}, {self.shape})" my_new_icon = Icon("blue", "circle") print(my_new_icon) # Icon(blue, circle) ``` --- ## Nodes, Trees, and Chess --- ### Nodes and Trees What is a tree? - A tree is a collection of nodes, each node can have a maximum of one parent. - The node at the top of a tree which has no parent is the root node. - Depending on the type of tree, a node can have multiple children, but in a binary tree it can have a maximum of two. - Nodes with no children are called leaf nodes. ``` x / \ x x / / \ x x x / / \ x x x ``` --- ### Depth first search 1. First, fully explore the left side of a tree and each subtree 2. Then move on to the right side. Traveral order: ``` 1 / \ 2 5 / / \ 3 6 9 / / \ 4 7 8 ``` --- ### Breadth first search 1. First, explore all children of a node. 2. Then move on to the next level of the tree. Traveral order: ``` 1 / \ 2 3 / / \ 4 5 6 / / \ 7 8 9 ``` --- ### Nodes and Trees ```javascript= // The start of a node class in JavaScript class Node { constructor(value) { this._value = value; this._parent = null; this._children = []; } } const node = new Node("x"); console.log(node) ``` --- ### Chess What does a knight do? [video description](https://www.chess.com/lessons/how-to-move-the-pieces/the-knight) ![knight_moves](https://i.imgur.com/o8Iejt1.png) --- ### Working on Knight Moves From a given position, the list of possible relative moves could be represented by the following: ```python= possible_moves = [ (-2, -1), (-2, 1), (-1, -2), (-1, 2), (1, -2), (1, 2), (2, -1), (2, 1), ] ``` --- ### Today's project We will be implementing nodes and trees so that we can represent all of the moves available to a knight as a tree. Each node will have a value that is a set of coordinates on the board, and its children will be the collection of positions a knight at that position could move to. Here are some additional notes for today's project: https://hackmd.io/@jpshafto/S1yh2mmuu