|
| 1 | +# Advanced looping |
| 2 | + |
| 3 | +https://www.learnpython.dev/03-intermediate-python/20-advanced-looping/10-list-comprehensions/ |
| 4 | + |
| 5 | +## List comprehensions |
| 6 | +Sorta similar to the `.map()` and `.filter()` and other array methods in JS. Convenient powerful shorthands for making lists. |
| 7 | + |
| 8 | +Say you want to iterate through a list, perform an operation on each item that produces a new value, and return a new list with those new values. You would use `.map()` in JS, but in python you can do a list comprehension. List comprehensions put an entire expression inside the square brackets: |
| 9 | + |
| 10 | +```python |
| 11 | +names = ['jimmy', 'susan', 'peter'] |
| 12 | +[name.upper() for name in names] # ['JIMMY', 'SUSAN', 'PETER'] |
| 13 | +``` |
| 14 | + |
| 15 | +So the syntax is kind of like: |
| 16 | +```python |
| 17 | +[ transform(thing) for thing in list_of_things ] |
| 18 | +``` |
| 19 | + |
| 20 | +```python |
| 21 | +[num * num for num in range(6)] # [0, 1, 4, 9, 16, 25] |
| 22 | +``` |
| 23 | + |
| 24 | +You can add a conditional filtering mechanism at the end of a list comprehension. Say you want to do the above squaring operation but only on the even numbers: |
| 25 | + |
| 26 | +```python |
| 27 | +[num * num for num in range(6) if num % 2 == 0] # [0, 4, 16] |
| 28 | +``` |
| 29 | + |
| 30 | +## Set comprehension |
| 31 | +Works very similarly for transforming a list into a set |
| 32 | + |
| 33 | +```python |
| 34 | +{ num * num for num in range(10) } |
| 35 | +# {0, 1, 64, 4, 36, 9, 16, 49, 81, 25} |
| 36 | +``` |
| 37 | + |
| 38 | +## Dictionary comprehension |
| 39 | +Construct a key:value expression in your transformation step to create a dictionary from a list: |
| 40 | +```python |
| 41 | +{ num: num * num for num } |
| 42 | +# {0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25} |
| 43 | +``` |
| 44 | + |
| 45 | +## Generator expressions (comprehensions) |
| 46 | +Generators are iterables that generate values on demand, instead of loading a massive list into memory. They return a generator, not a list. |
| 47 | + |
| 48 | +Wrap the comprehension expression in parentheses instead of square brackets to make a generator expression. |
| 49 | + |
| 50 | +```python |
| 51 | +gen = (x ** 2 for x in range(10) if x % 2 == 0) |
| 52 | +type(gen) # <class 'generator'> |
| 53 | +``` |
| 54 | + |
| 55 | +You can use a generator in place of list in a lot of expressions. Note that once a generator has been fully iterated, it is now empty and functions as an empty array when used: |
| 56 | + |
| 57 | +```python |
| 58 | +gen = (x ** 2 for x in range(10) if x % 2 == 0) |
| 59 | +# use the above generator to make a set. each number is computed on the fly |
| 60 | +{ v for v in gen } # {0, 64, 4, 36, 16} |
| 61 | + |
| 62 | +max(gen) # ValueError: max() arg is an empty sequence |
| 63 | + |
| 64 | +# reinstantiate generator (important because the above loop ran through the iterator leaving it empty) |
| 65 | +gen = (x ** 2 for x in range(10) if x % 2 == 0) |
| 66 | +max(gen) # 64 |
| 67 | +``` |
| 68 | + |
| 69 | +Generator are generated when you put them in a comprehension, or a for loop, or when calling `next(generator)` is called. |
| 70 | + |
| 71 | +## Slicing |
| 72 | +Any ordered sequence (list, tuple, or string ) can be sliced. It's not a method, it's just a special indexing syntax. You apply the square bracket syntax with `[starting_index: ending_index: step]` where `starting_index` is included by `ending_index` is not. `step` defaults to 1: |
| 73 | + |
| 74 | +```python |
| 75 | +letters = ['a', 'b', 'c', 'd', 'e'] |
| 76 | +letters[0:2] # ['a', 'b'] |
| 77 | +letters[1:3] # ['b', 'c'] |
| 78 | +# when starting index is omitted, it starts from the beginning |
| 79 | +letters[:3] # ['a', 'b', 'c'] |
| 80 | +# when ending index is omitted, it goes to the end |
| 81 | +letters[2:] # ['c', 'd', 'e'] |
| 82 | +letters[-2:] # ['d', 'e'] |
| 83 | +``` |
| 84 | + |
| 85 | +Assigning `new_list = some_list`, we have not created two lists. We have just created a pointer to `some_list`. Thus any mutations to `new_list` would also be reflected on `some_list`. Sometimes you want to clone a list so that you can modify the clone without updating the original. __Slicing with both indeces omitted does this__ |
| 86 | + |
| 87 | +```python |
| 88 | +some_list = [1, 2, 3] |
| 89 | +a_new_list = some_list[:] |
| 90 | +# only modifies `a_new_list` |
| 91 | +a_new_list.append(4) |
| 92 | +# use a -1 step to clone a list in reverse |
| 93 | +reverse_list = some_list(::-1) |
| 94 | +# [3, 2, 1] |
| 95 | +``` |
| 96 | + |
| 97 | +## Zip |
| 98 | +You have two different lists, and you want to unify them by index, where the first item in list 1 is grouped with the first item in list 2, etc. `zip(l1, l2)` does that. |
| 99 | +```python |
| 100 | +players = ['Susy', 'Alex', 'Roberto'] |
| 101 | +scores = [88, 78, 92] |
| 102 | +zipped = zip(players, scores) |
| 103 | +type(zipped) # <class 'zip'> |
| 104 | +``` |
| 105 | + |
| 106 | +A zip object is kind of like an iterable. It's not immediately explorable without throwing it in a loop, comprehension, or calling `next(zipped)`. What you'll see is that each item in the zip iterable is a tuple: |
| 107 | + |
| 108 | +```python |
| 109 | +# convert iterable to list to see what's inside |
| 110 | +scorecards = list(zip(players, scores)) |
| 111 | +# [('Susy', 88), ('Alex', 78), ('Roberto', 92)] |
| 112 | + |
| 113 | +# or turn it into a dict |
| 114 | +scorecards = list(zip(players, scores)) |
| 115 | +# {'Susy': 88, 'Alex': 78, 'Roberto': 92} |
| 116 | +``` |
0 commit comments