Dazbo's Advent of Code solutions, written in Python
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)