---
# System prepended metadata

title: Python Collections and Built-ins

---

<style>
    .present {
        text-align: left;
    }
    img[alt=set_operations] {
        width: 60%;
        
    }
</style>

# Python Collections and Built-ins
## Week 17 Day 3


---

## Videos 1 (14 minutes)
Watch:
- Lists in Python (10:15)


---

### Lists

Lists are mutable, ordered collections (like arrays in JavaScript). Use square brackets to make lists.

```python=
empty = []
print(empty)   # []
```

Values are separated by commas.
```python=
fruits = ["banana", "apple", "kiwi"]
print(fruits)  # ["banana", "apple", "kiwi"]
```


---

### Indexing with lists

To get individual values:
- `list_name[single_index]`
```python=
fruits = ["banana", "apple", "kiwi", "mango", "tangerine"]

print(fruits[1])   # apple
print(fruits[-1])  # tangerine
```

---

### Indexing with lists (ranges)

To get a range of values:
```python=
fruits = ["banana", "apple", "kiwi", "mango", "tangerine"]

# list_name[inclusive_start:exclusive_end]
print(fruits[1:4])    # ["apple", "kiwi", "mango"]
print(fruits[-1:])    # ["tangerine"]

# list_name[inclusive_start:exclusive_end:step_size]
print(fruits[::-1])   # ["tangerine', "mango', "kiwi", "apple", "banana"]
print(fruits[3::-1])  # ["mango", "kiwi", "apple", "banana"]
```

---

### Built-in functions (`len()`)
We can use the `len()` function to get the length of lists.

```python=
fruits = ["banana", "apple", "kiwi", "mango", "tangerine"]

print(len(fruits))       # 5
print(len(fruits[1:4]))  # 3
```

---

## Practices 1 (20 min)

- Explore The List (5:00)
- Return First Element Of A List (2:00)
- First And Last Entries (1:00)
- Check Nested Arrays (10:00)

---


## Lecture Videos 2 (7 minutes)
Watch:
- Doing Math With Lists (5:07)

Discuss code only:
- Mutability Of and Sorting Lists (12:51)


---


### List methods

Use `append` to add items to the end of a list (like `push` in JavaScript). This mutates the list.

```python=
fruits = ["banana", "apple", "kiwi"]

fruits.append("apple")
print(fruits)  # ["banana", "apple", "kiwi", "apple"]
```


Use `remove` to delete the first instance of a given value. (Also mutates the list).
```python=
fruits.remove("apple")
print(fruits)  # ["banana", "kiwi", "apple"]

# Error if value is not present in list
fruits.remove("mango") # ValueError: list.remove(x): x not in list
```

(for more list methods, check out [the documentation here](https://docs.python.org/3/tutorial/datastructures.html).)

---

### Sorting lists

The `.sort()` method will sort a list, mutating the list

```python=
fruits = ["mango", "banana", "apple", "kiwi"]
fruits.sort()
print(fruits)  # ["apple", "banana", "kiwi", "mango"]
```

---

### Sorting lists (custom sort)

You can provide a key to change how the list is sorted
- the key is a function
- rather than sort the list values directly, the sort will apply the key function to each value and sort based on the resulting value
- this will not mutate the values in the list (just the order), it will just use the calculated values in the comparisons

```python=
names = ["janie", "Julie", "Allie", "pamela"]
names.sort()
print(names)  # ["Allie", "Julie", "janie", "pamela"]
# ensure a case-insensitive sort with the `.lower` string method
names.sort(key=str.lower)
print(names)  # ["Allie", "janie", "Julie", "pamela"]
```

---


### Sorting, continued

The `sorted()` function can be used to sort lists without mutating. It also works with other iterable types (e.g. tuples).

```python=
fruits = ["mango", "banana", "apple", "kiwi"]
sorted_fruits = sorted(fruits)
reversed_fruits = sorted(fruits, reverse=True)
print(sorted_fruits)   # ['apple', 'banana', 'kiwi', 'mango']
print(reversed_fruits) # ['mango', 'kiwi', 'banana', 'apple']
```

- note that the parameters `key` and `reverse` work with both `.sort()` and `sorted()`


---

### Sorting lists (fun fact)

Sorting in Python uses [Timsort](https://en.wikipedia.org/wiki/Timsort). (The inventor's name is Tim. Really.)

It is based on a combination of merge sort and insertion sort, and it is designed to perform optimally on real data.

---

### More built-in functions
`sum()`, `min()`, `max()`, can be used to compute values over lists.

```python=
values = [49, 22, 13, 25]
print(values)       # [49, 22, 13, 25]

total_value = sum(values)
print(total_value)  # 109

top_value = max(values)
print(top_value)    # 49

min_value = min(values)
print(min_value)    # 13

avg_val = sum(values)/len(values)
print(avg_val)     # 27.25
```

---

## Practices 2 (15 min)

- Find The Smallest Number In A List (5:00)
- Sum The Elements Of A List (5:00)
- Maximum Difference (5:00)


---

## Lecture Videos 3 (22 minutes)

Watch:

- Tuples in Python (17:07)
- Special Case: Single Item Tuple (2:46)

---

### Tuples

- Tuples are an ordered, immutable collection type. 
- They are defined using parentheses `()`, with values separated by a comma.

```python=
a = (1, 2, 3, 4, 5, 6, 7, 8, 9)
b = ('a', 'b', 'c', 'd', 'e')
c = 10, 20, 30

print(a)  # (1, 2, 3, 4, 5, 6, 7, 8, 9)
print(b)  # ('a', 'b', 'c', 'd', 'e')
print(c)  # (10, 20, 30)
```

---

### Tuples are immutable [1/2]

- You cannot append, remove, or sort the tuple in place. 
- However, they are not constants.

```python=
tup = ("tall", "wide")
tup = ("small", "narrow")  # no error, this works
print(tup)                 # ("small", "narrow")
tup += ("tall", "wide")    # no error, this works
print(tup)                 # ("small", "narrow", "tall", "wide")
```

---

### Tuples are immutable [2/2]

Mutable items in a tuple can be mutated:

```python=
tup = ([1, 2, 3], "hello")
print(tup[0])  # [1, 2, 3]
tup[0].append(4)
print(tup[0])  # [1, 2, 3, 4]
print(tup)     # ([1, 2, 3, 4], "hello")
```

---

### Sorting

The `sorted()` function works on tuples, just like lists. This will return a list by default.

```python=
fruits = ("banana", "apple", "kiwi")
print(sorted(fruits))  # ['apple', 'banana', 'kiwi']
sorted_fruits = tuple(sorted(fruits))
print(sorted_fruits)
print(fruits)
```

---

### Returning tuples

- Using a comma in your return statement will return a tuple
- You can store the tuple in one variable or destructure the tuples values into multiple variables.

```python=
def min_max(numbers):
    return min(numbers), max(numbers)

values = (14, 2, -2, 3.3, -8, -25, 9, 0)

low_and_high = min_max(values)
print(low_and_high)  # (-25, 14)

lowest, highest = min_max(values)
print(lowest)        # -25
print(highest)       # 14
```

---

### Singleton tuples

Since parentheses are also used for grouping, python won't interpret a single like you might expect.

```python=
hopefully_a_tuple = (5)
print(hopefully_a_tuple)  # 5
# whoops! not a tuple
```

You need to use a comma so that python can recognize that a tuple is present.

```python=
hopefully_a_tuple = (5,)
print(hopefully_a_tuple)  # (5,)
# nice, that is a tuple
also_a_tuple= 5,
print(also_a_tuple) # (5,)
# awesome, that is also a tuple
```

---

## Practices 3 (5 min)

- Explore The Tuple (5:00)

---

## Lecture Videos 4 (10 minutes)

Watch:

- Ranges in Python (9:00)

---

### Ranges

- An immutable list of numbers in order
- Arguments:
  - start (default 0)
  - stop (required)
  - step (default 1)
- Can go in forward or reverse order
- Range is exclusive(the stop argument is not included in the returned list)

---

### Declaring Basic Ranges

```python=
numbers_range = range(10)
print(numbers_range) # range(0, 10)
print(list(numbers_range)) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

starting_at_one = range(1, 11)
print(list(starting_at_one)) # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
```

---

### Reverse Ranges

```python=
reversed_maybe = range(51, 5)
print(list(reversed_maybe)) # []

reversed = range(51, 5, -1) 
print(list(reversed)) # [51, 50, ... 9, 8, 7, 6]
```

---

### Iterating over Collections and Using Ranges

```python=
items = ['a', 'b', 'c']

for item in items:
    print(item)

for i in range(len(items)):
    print(i, items[i])


for i in range(1, 10, 2):
    print(i)
```

---

## Practices 4 (7 min)

- Explore The Range (5:00)
- Range Loops (2:00)

---

## Lecture Videos 5 (10 minutes)

Watch:

- Dictionaries in Python (8:31)

Discuss Code Only:
- More Ways To Make A Dictionary (10:08)

---

### Dictionaries

- Dictionaries are ordered and mutable. 
- They consist of pairs of keys and values. 
    - The keys must be "hashable" (immutable) values
    - e.g. keys can be strings, numbers, tuples, booleans, but not lists or dictionaries
- Dictionaries use curly braces, similar to JavaScript objects, although keys do not have to be strings.
    - but if the keys _are_ strings, they must use quotes, unlike JS

---

### Declaring Dictionaries
```python=
a = {'one':1, 'two':2, 'three':3}
b = dict(one=1, two=2, three=3)
c = dict([('two', 2), ('one', 1), ('three', 3)])
print(a == b == c)   # True
```
If keys and values are the same, they are considered equivalent, doesn't matter how they were defined

---

### Using `[]` and `.get()` to index into a Dictionary
- Unlike javascript in Python you cannot index into a dictionary with dot notation
```python=
my_dict = {'one':1, 'two':2, 'three':3}
print(my_dict.one) #AttributeError: 'dict' object has no attribute 'one'
print(my_dict['one']) #1
print(my_dict['new']) # KeyError: 'new'
print(my_dict.get('one', None)) # 1
print(my_dict.get('gorilla', None)) # None
```

---

### Adding, Updating, Deleting Data in Dictionaries

- `del` keyword to delete a key/value pair
- `[]` to add or update a key/value pair
```python=
my_dict = {
    "word": "cooool",
    1: "one",
    False: 42,
    ("tuple", "keys"): ["lists", "can", "be", "values", "not", "keys"],
    None: {"key": "value"}
}

my_dict[1] = "two" # Updates value
print(my_dict[1]) # "two"
my_dict["new_key"] = "new_value" #Adds key/value
del my_dict["word"] # Deletes key/value
print(my_dict) # I promise, all the changes are there!
```

---

### Using `in`
- `in` allows you to check if a value is a key in the dictionary
```python=
print(1 in {1: "one", 2: "two"})    # True
print("1" in {1: "one", 2: "two"})  # False
print(4 in {1: "one", 2: "two"})    # False
print("one" in {1: "one", 2: "two"})# False
```

---

### .items()
 - `.items()` returns a list of tuples
     - Each tuple contains the key/value pair of the item in the dictionary
```python=
book = {
    'title': 'Goodnight Moon',
    'ratings': 7492,
    'stars': 4.8,
    'author': {'firstName': 'Margaret', 'lastName': 'Wise Brown'},
    'images': ['goodnight1.png', 'goodnight2.png'],
}
    
for key, value in book.items():
    print('key:', key, 'value:', value)

for key in book:
     print('key:', key, 'value:', book[key])

```

---

### `.keys()` and `.values()`
- `.keys()` returns a list of the keys in the dictionary
- `.values()` returns a list of the values in the dictionary
```python=
book = {
    'title': 'Goodnight Moon',
    'ratings': 7492,
    'stars': 4.8,
    'author': {'firstName': 'Margaret', 'lastName': 'Wise Brown'},
    'images': ['goodnight1.png', 'goodnight2.png'],
}

for value in book.values():
    print('value:', value)

for key in book.keys():
    print('key:', key)
```

---

## Practices 5 (15 minutes)
- Explore The Dictionary (5:00)
- Does The Dictionary Have A Key? (2:00)
- Is The Dictionary Empty? (1:00)
- Create Name Tag (5:00)


---

## Lecture Videos 6 (18 minutes)
Watch:
- Sets in Python (12:19)
- Using Sets (2:42)

Discuss code only:
- Combining Data Structures (7:32)
- Stack And Queue Overview (13:26)


---

### Sets

Sets are mutable, unordered collections where all elements are unique. 

Sets use curly braces, like a dictionary. You can create an empty set with `set()`.

While sets can be mutated, the individual elements must be immutable types.

```python=
not_an_empty_set = {}
print(type(not_an_empty_set))  # <class 'dict'>

empty_set = set()
print(type(empty_set))         # <class 'set'>

set_with_elements = {1, "hello", None}
print(set_with_elements)       # {1, None, "hello"}

no_duplicates = {1, 1, 1}
print(no_duplicates)           # {1}
```

---

### Set operations
#### Union `(a | b)`
![set_operations](https://i.imgur.com/5bY2S1u.png)

```python=
a = {1, 2, 3}
b = {3, 4, 5}
print(a | b)       # {1, 2, 3, 4, 5}
print(a.union(b))  # {1, 2, 3, 4, 5}
# order of a and b does not matter
```

---

### Set operations
#### Intersection `(a & b)`
![set_operations](https://i.imgur.com/dyvvZAv.png)
```python=
a = {1, 2, 3}
b = {3, 4, 5}
print(a & b)              # {3}
print(a.intersection(b))  # {3}
# order of a and b does not matter
```

---

### Set operations
#### Symmetric difference `(a ^ b)`
![set_operations](https://i.imgur.com/k2leBRy.png)
```python=
a = {1, 2, 3}
b = {3, 4, 5}
print(a ^ b)                      # {1, 2, 4, 5}
print(a.symmetric_difference(b))  # {1, 2, 4, 5}
# order of a and b does not matter
```

---

### Set operations
#### Difference `(a - b)` 
![set_operations](https://i.imgur.com/soh0G8N.png)
```python=
a = {1, 2, 3}
b = {3, 4, 5}
print(a - b)            # {1, 2}
print(a.difference(b))  # {1, 2}
```


---

### Set operations
#### Difference `(b - a)`
![set_operations](https://i.imgur.com/UhNIrjj.png)
```python=
a = {1, 2, 3}
b = {3, 4, 5}
print(b - a)            # {4, 5}
print(b.difference(a))  # {4, 5}
```


---

### Sets operations, continued
With multiple operators, evaluation will happen from left to right.
```python=
a = {1, 2, 3}
b = {3, 4, 5}
c = {5, 6, 7}
print(a & b | c)    # {3, 5, 6, 7}
print((a & b) | c)  # {3, 5, 6, 7}
print(a & (b | c))  # {3}
```

---

### Creating sets from other iterables

You can use the `set()` function to create new sets from other iterable types.

```python=
my_list = [1, 2, 3, 4, 5, 5]
print(set(my_list))    # {1, 2, 3, 4, 5}
my_string = "hello"
print(set(my_string))  # {'l', 'h', 'o', 'e'}
my_tuple = (1, 1, 1)
print(set(my_tuple))   # {1}
my_dict = {"hello": "value", "goodbye": "value"}
print(set(my_dict))    # {'goodbye', 'hello'}
```

---

### Code from "Combining Data Structures"

```python=
# posts is a list of dictionaries, some of the values in that list
posts = [
    {
        "title": "All About Lists",
        "tags": ("fun", "informative", "lists")
    },
    {
        "title": "Tuple Trouble",
        "tags": ("fun", "tuples")
    },
    {
        "title": "Sparkling Sets",
        "tags": ("informative", "numbers")
    },
]

all_tags = []  # list to hold all tags
for i in range(len(posts)):
    print(posts[i]["tags"])
    all_tags.extend(posts[i]["tags"])

print(all_tags)
print(set(all_tags))
all_tags = list(set(all_tags))
all_tags.sort()
print(all_tags)
```

---

### Code example from "Stack and Queue Overview"
#### Interuption stack
```python=
teller = []

teller.append("Greet Customer")
print(teller)
teller.pop()
print(teller)
teller.append("Process Deposit")
print(teller)
teller.append("Phone Ringing")
print(teller)
teller.pop()
teller.append("Greet Caller, Listen, Answer Question")
print(teller)
teller.pop()
print(teller)
teller.pop()
print(teller)
```

---

### Code example from "Stack and Queue Overview"
#### Processing queue
```python=
processor = []
processor.append({'type':'page','path':'','header':[],'cookies':[]}),
processor.append({'type':'api', 'function':'', 'parameters':[]})
processor.append({'email':'email','address':'bob@gmail.com','subject':''})
print("PROCESSOR LIST", processor)

for i in range(len(processor)):
    item = processor.pop(0)
    print("PROCESSING ITEM", item)
    print("REMAINING LIST", processor)
```


---


## Lecture Videos 7 (30 minutes)
Watch:
- Built-Ins: All And Any (8:00)
- Built-Ins: Filter and Map (10:32)
- Built-Ins: Custom Sort (9:05)

Discuss code only:
- Local Packages And Modules


---

### Built-in functions (`all()`)
The `all()` function checks that there are _no_ falsey items in the provided collection.

```python=
test1 = {"item", "truthy", ""}
test2 = []
test3 = [[]]
print(all(test1), test1)
print(all(test2), test2)
print(all(test3), test3)
```

---

### Built-in functions (`all()`)
Answers:

```python=
test1 = {"item", "truthy", ""}
test2 = []
test3 = [[]]
print(all(test1), test1)  # False {"", "truthy", "item"}
print(all(test2), test2)  # True []
print(all(test3), test3)  # False [[]]
```

---

### Built-in functions (`any()`)
The `any()` checks that there is at least one truthy item in the provided collection

```python=
test1 = ["item", [], []]
test2 = []
test3 = [[]]
print(any(test1), test1)
print(any(test2), test2)
print(any(test3), test3)
```

---

### Built-in functions (`any()`)
Answers:

```python=
test1 = ["item", [], []]
test2 = []
test3 = [[]]
print(any(test1), test1)  # True ['item', [], []]
print(any(test2), test2)  # False []
print(any(test3), test3)  # False [[]]
```

---

### Built-ins (`filter()`)
The `filter()` function takes a function and an iterable, and it returns a "filter object".

The returned collection includes only the items which, when the function parameter was applied to them, returned a truthy value.

```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-ins (`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-ins (`map()`)
The `map()` function takes a function and an iterable, and it returns a "map object" that transforms each value from the original iterable, according to the provided function.
```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-ins (`zip()`)
The `zip()` function takes two iterables and returns a "zip object" that pairs values at corresponding indices.

You can convert the "zip object" into a list of tuples with the `list()` function.

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



---

### Basic imports in Python

Basic imports in Python are backwards from JavaScript. 
```python=
# from module_name import variable_name
from random import randint

print(randint(0, 10))
```

You can also import an entire module, and then access all the values from it using dot notation.
```python=
import random

print(random.randint(0, 10))
```


---

### ~~Exports in Python~~

Unlike JavaScript, Python does not require exports. All of the objects, classes, functions, etc. that are defined in a module are automatically available to import.

---


## Practices 7 (5 min)
- All Occurrences Of A Value In A List (5:00)



---

## Summary Practices (60 minutes)

- Guess A Number Game
- Bonus: Track The Robot
- Bonus: Averages


## Summary Quizzes (5 minutes)
- Structured Data Quiz (5:00)