# Learning Python with Advent of Code Walkthroughs

Dazbo's Advent of Code solutions, written in Python

# The Python Journey - Comprehensions ## Overview

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

## List Comprehension Example

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

## Aggregate Functions

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

## Nested Comprehension

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

## Multi-Sequence Comprehension

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

A couple of examples…

### Creating Cartesian Coordinates

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

### Creating a Set of Deltas to Adjacent Points

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

## Dictionary Comprehension

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

### Example: Square Numbers

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

### Inverted Key:Value Pairs

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()}
``````

## Filtering Comprehensions

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

## Aggregating Comprehensions

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