Dazbo's Advent of Code solutions, written in Python

- Overview
- List Comprehension Example
- Aggregate Functions
- Finding Adjacent Points Example
- Nested Comprehension
- Multi-Sequence Comprehension
- Dictionary Comprehension
- Filtering Comprehensions
- Aggregating Comprehensions

In Python, a **comprehension** is a convenient shorthand for creating a collection, by iterating through an existing iterable.

This is easier to explain with an example!

Here’s how we might use a `for loop`

to determine the first 10 *cube* numbers, and store them in a list:

```
cube_numbers = []
for num in range(1, 11):
cube_numbers.append(num**3)
print(cube_numbers)
```

The output looks like this:

```
[1, 8, 27, 64, 125, 216, 343, 512, 729, 1000]
```

But we can simplify the code, and make it look a bit more like *plain English*, by using a *list comprehension*:

```
cube_numbers = [num**3 for num in range(1, 11)]
print(cube_numbers)
```

The output is identical! Cool, right?

So, **the general construct for a list comprehension** is:

```
new_list = [expr(item) for item in iterable]
```

Note the use of square brackets around the comprehension. Thus, this comprehension returns a `list`

.

Add we can apply aggregate functions, like we would with any other list. For example, if wanted to calculate the sum of the first 10 cube numbers:

```
total = sum([num**3 for num in range(1, 11)])
print(total)
```

Output:

```
3025
```

When we’re applying an aggregate function around a comprehension, we can omit the square brackets. So, we can actually just write this:

```
total = sum(num**3 for num in range(1, 11))
print(total)
```

This example starts by creating a `Point`

class. It’s just a dataclass. Then I create a list of `vectors`

, which is made up of four `(x,y)`

vectors to get from any given point to all its adjacent orthogonal points.

```
@dataclass
class Point():
x: int
y: int
vectors = [
(0, 1), # up
(1, 0), # right
(0, -1), # down
(-1, 0) # left
]
point = Point(3,2)
print(f"Starting point: {point}")
neighbours = [Point(point.x+dx, point.y+dy) for dx, dy in vectors]
print(f"Neighbours: {neighbours}")
```

We then create a starting `Point`

object, at location `3,2`

. The cool part is where we use a `list comprehension`

to iterate through the four vectors, with each returned as a `dx, dy`

tuple. We then add each `dx`

and `dy`

to our starting point. The result is a list of four new points, as required.

The output:

```
Starting point: Point(x=3, y=2)
Neighbours: [Point(x=3, y=3), Point(x=4, y=2), Point(x=3, y=1), Point(x=2, y=2)]
```

This is a comprehension nested in another comprehension. It **creates a list with more than one dimension**.

For example, this code creates a list of five items, with each item itself a list of three items.

```
vals = [[x*y for y in range(3)] for x in range(5)]
print(vals)
```

The above is equivalent to this nested loop:

```
vals = []
for x in range(5):
inner = []
for y in range(3):
inner.append(x*y)
vals.append(inner)
```

Output:

```
[[0, 0, 0], [0, 1, 2], [0, 2, 4], [0, 3, 6], [0, 4, 8]]
```

This is a way to **create a single list from nested loops**.

A couple of examples…

Here we create a list of `(x,y)`

tuples, with x from 0-4 (inclusive) and y from 0-2 (inclusive).

```
# Create a list of point tuples
points = [(x, y)
for x in range(5)
for y in range(3)]
# the above is equivalent to
points = []
for x in range(5):
for y in range(3):
points.append((x, y))
```

Output:

```
[(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2), (2, 0), (2, 1), (2, 2), (3, 0),
(3, 1), (3, 2), (4, 0), (4, 1), (4, 2)]
```

Here we create a list of (dx,dy) values, in order to represent the delta to get from a coordinate to all 8 adjacent coordinates. I.e.

```
-1, 1 0, 1 1, 1
-1, 0 0, 0 1, 0
-1,-1 0,-1 1,-1
```

```
delta = 1
adjacent_deltas = [(dx,dy) for dx in range(-delta, delta+1)
for dy in range(-delta, delta+1)
if (dx,dy) != (0,0)]
```

Output:

```
[(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]
```

Just as we can use a *comprehension* to generate a list, we can also use a *comprehension* to generate a dictionary.

The general syntax is:

```
some_dict = {key_expr(item): value_expr(item) for item in iterable}
```

For example, if we had a function called `func()`

that we can use to generate a value for any given key, we could create a dictionary like this:

```
my_dict = {key:func(key) for key in some_range}
```

```
my_dict = {i: i**2 for i in range(10)}
```

If we print the value of `my_dict`

, it looks like this:

```
{0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81}
```

Imagine we have a dictionary where all the values are unique, and we would like the values to become the keys, and *vice versa*. We can do it like this:

```
inverted_dict = {value: item for item, value in my_dict.items()}
```

In order to return only the values that match a filter condition, the general construct is:

```
vals = [expression for value in iterable
if condition]
```

What if we wanted to return a value when the condition doesn’t match? We can do this:

```
vals = [expression if condition
else value for value in iterable]
```

Here we want to add up the values, but only for keys that match a condition:

```
sum_of_values = sum([fields[x] for x in fields.keys() if x.startswith("departure")])
```

And here are two equivalent ways to count values that match a boolean condition:

```
valid_for_posn = sum(1 for word in words if word.is_valid_for_posn())
```

```
valid_for_posn = sum(word.is_valid_for_posn() for word in words)
```