---
# System prepended metadata

title: (Archive) Python Classes and Comprehensions

---

<style>
    .present {
        text-align: left;
    }
    img[alt=knight_moves] {
        width: 400px;
        
    }
</style>


# (Archive) 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


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


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
```

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 = 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}
```

---

### 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 (collected args):", 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