<style> .present { text-align: left; } img[alt=knight_moves] { width: 400px; } </style> # Python Classes and Comprehensions ## Week 17 Day 4 --- ## Lecture videos 1 (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 Widget: # more code to come ``` Looks a lot like JavaScript ```javascript= // javascript comparison class Widget { // 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 Widget: def __init__(self): # more code to follow ``` ```javascript= // javascript comparison class Widget { constructor() { // more code to follow } } ``` --- ### the `__init__()` method Python's constructor method is called `__init__()`. ```python= # python example class Widget: def __init__(self, color, shape): self.color = color self.shape = shape ``` ```javascript= // javascript comparison class Widget { 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 Widget: 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_widget = Widget("blue", "square") # both below do the same thing some_widget.my_method("other argument") Widget.my_method(some_widget, "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 Widget: 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_widget = Widget("blue", "circle") ``` ```javascript= // javascript comparison const myNewWidget = new Widget("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 Widget: def __init__(self, color, shape): self.color = color self.shape = shape class Gadget(Widget): def __init__(self, color, shape, noise): super().__init__(color, shape) self.noise = noise thingie = Gadget("purple", "spiral", "whrrrrrr") print(thingie) # <__main__.Gadget object at 0x105103d60> print(thingie.color, thingie.shape, thingie.noise) # purple spiral whrrrrrr ``` --- ### Getters and setters Using getters and setters lets you have methods that behave like properties—these methods will run without being invoked when you attempt to access or update the value. This gives your class a convenient interface for more complicated logic that happens behind the scenes, and it could also be useful for protecting certain "private" values on instances of your class. Python doesn't have any actual "private" attributes—all of the attributes can be accessed or changed from outside the class or instance—so this isn't a security feature. It's just so that other users who interact with your code will understand how it is supposed to be used. --- ### Without getters and setters ```python= class Widget: def __init__(self, color, shape): self.color = color self.shape = shape self.secret_property = None def get_secret_property(self): # log everytime someone accesses the secret property print("oooh someone tried to get the secret property") return self.secret_property def set_secret_property(self, value): # super secret hashing strategy self.secret_property = str(value) + "abcde" * 3 return trinket = Widget("orange", "cylinder") print(trinket.get_secret_property()) trinket.set_secret_property("beep") print(trinket.get_secret_property()) ``` --- ### With getters and setters ```python= class Widget: def __init__(self, color, shape, regular_property): self.color = color self.shape = shape self.regular_property = regular_property @property def regular_property(self): # log everytime someone accesses the secret property print("oooh someone tried to get the secret property") return self.secret_property @regular_property.setter def regular_property(self, value): # super secret hashing strategy self.secret_property = str(value) + "abcde" * 3 return trinket = Widget("orange", "cylinder", "hops") print(trinket.regular_property) trinket.regular_property = "beep" print(trinket.regular_property) ``` --- ### "Private" properties By convention, if you have private properties you want to protect, use a single underscore to indicate that. We could replace the `secret_property` on our widget with `_property` to follow this convention, e.g.: ```python= @property def regular_property(self): # log everytime someone accesses the secret property print("oooh someone tried to get the secret property") return self._property ``` --- ### Getters Using the `@property` decorator on a method creates a **getter**. The getter will not need to be invoked: the method will run when you attempt to access the value. e.g. ```python= print(trinket.regular_property) ``` causes this method to run. ```python= @property def regular_property(self): # log everytime someone accesses the secret property print("oooh someone tried to get the secret property") return self.secret_property ``` --- ### Setters The decorator used to create a **setter** is `@<getter_method_name>.setter`. ```python= @regular_property.setter def regular_property(self, value): # super secret hashing strategy self.secret_property = str(value) + "abcde" * 3 return ``` You do not need to have a setter in order to have a getter, but you do need to have a getter to have a setter. The setter method will run when you try to change the value. ```python= trinket.regular_property = "beep" # equivalent to this line without setter trinket.set_secret_property("beep") ``` --- ### Duck-Typing (but for real this time) "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 Widget: def __init__(self, color, shape): self.color = color self.shape = shape my_new_widget = Widget("blue", "circle") print(my_new_widget) # <__main__.Widget object at 0x1071cebb0> ``` --- ### Duck-typing You can use the `__repr__` method that will let you control what gets printed for your class. ```python= class Widget: def __init__(self, color, shape): self.color = color self.shape = shape def __repr__(self): return f"Widget({self.color}, {self.shape})" my_new_widget = Widget("blue", "circle") print(my_new_widget) # Widget(blue, circle) ``` --- ## Lecture videos 2 (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 = [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} ``` --- ### Collecting Function Arguments In JavaScript, we could use the spread/rest operator (`...`) to collect a variable number of function arguments into an array. ```javascript= // javascript example const myHobbies = (name, ...args) => { console.log("name (first positional arg):", name) console.log("collected arguments:", args) return `${name}'s favorite hobbies are ${args.join(', ')}` } console.log(myHobbies("Mitchell", "swimming", "cycling", "making pizza", "BBQ")) ``` In Python, we can use the "splat" operator (`*`) and the "double-splat" operator (`**`) to do something similar. --- ### Positional arguments In Python, if we want to collect some variable number of anonymous positional arguments, we use the splat operator after all of the named positional arguments. Note that the `args` will be collected into a tuple, not a list. ```python= def my_hobbies(name, *args): print("name (first positional arg):", name) print("collected positional args:", args) return f"{name}'s favorite hobbies are {', '.join(args)}" print(my_hobbies("Mitchell", "swimming", "cycling", "making pizza", "BBQ")) # name (first positional arg): Mitchell # collected positional args: ('swimming', 'cycling', 'making pizza', 'BBQ') # Mitchell's favorite hobbies are swimming, cycling, making pizza, BBQ ``` --- ### Keyword Arguments In addition to positional arguments (`args`), Python also has keyword arguments (`kwargs`). Anonymous keyword arguments can be collected using the `**` operator. Keyword arguments must follow all positional arguments, so the input order is always: - `named_arguments, *args, named_keyword_args=value, **kwargs` The `**` operator collects keyword arguments into a dictionary. ```python= def my_hobbies(name, *args, age=10, **kwargs): print("age (kwarg):", age) print("collected kwargs:", kwargs) info_dict = { "hobbies": f"{name} favorite hobbies are {', '.join(args)}", "stats": {**kwargs, "age": age} } return info_dict print(my_hobbies("Mitchell", "swimming", "cycling", "making pizza", "BBQ", age=30, height=60, glasses=False)) ``` --- ## 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