---
# System prepended metadata

title: '(Knight''s Travail version) More Built-ins, Comprehensions, and Classes'

---

<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