Python patterns I learned during 2023's Advent of Code
Last year, I used python for Advent of Code and loved it. It felt like there was always a beautiful built-in way of doing whatever it was I was trying to do. I wanted to count something? Counter
? Compare it against something else? It just works. Create a set? Easy. Navigate in 2d space? Imaginary numbers to the rescue. I doubt any of this will be useful for someone who writes python day to day or who wants to use python to solve problems that don't involve feisty elves, but if you're thinking about exploring python for Advent of Code, some of these tricks might help:
-
Split on
\n\n
to make processing data easier. This is an Advent of Code specific technique, but there were will often be multiple sections to the input, and splitting on\n\n
is an easy way to break up those sections for processing. -
Speaking of parsing the input, pulling the numbers out of a sentences comes up over and over again:
import re map(int, re.findall("-?\d+", "Move 2 paces to the right. Then -3 paces up."))
-
When a regular expression doesn't make sense,
"20".isdigit()
is worth knowing about. (This will not handle negative numbers) -
One year when writing JS, I typed out the alphabet for a problem that needed it, and it took me a long time to realize I was missing an R. You can grab an alphabet pretty easily from
string
:from string import ascii_lowercase
, but you can also useord
andchr
to move back and forth between an ordinal number and a character."".join(chr(n) for n in range(ord("A"), ord("A") + 26))
-
When a problem requires navigating in 2D space, I love using imaginary numbers because they make it easy to add a vector to a point to get a new point. It's a small thing, but the less code I need to write, the fewer mistakes I can make:
curr = 0 + 1j for d in ((1, -1, 1j, -1j)): do_thing(curr + d) # compared to curr = (0, 0) for dx, dy in ((1, 0), (-1, 0), (0, 1), (0, -1)): do_thing((curr[0] + dx, curr[1] + dy))
-
I use imaginary numbers rather than tuples for navigating 2D space, but I still love tuples. One thing I didn't realize at first is that you can use tuples as keys to objects without needing to convert them to strings first.
-
Speaking of data structures, all of the data structures in collections are fantastic.
a.
Counter
does exactly what it says on the tin.Counter("aaabbc")
can even be compared against anotherCounter
:Counter("aaa") > Counter("aa") # True
.b.
defaultdict
is essential when setting up data structures.defaultdict(list)
will let you avoid writing code likemy_dict["key"] = my_dict.get("key", [])
to make sure that a key exists before accessing it. -
heapq ("heap queue") is also worth knowing if you need to implement Djikstra's or anything else that needs a heap. In JS-land, manually implementing a heap to solve a problem is a bummer.
heapq
's usage feels a bit odd: you start with a list, and then call heapq methods on it that use that list as the storage medium for the heap. It's not its own data structure:h = [] heappush(h, (5, 'santa')) heappush(h, (7, 'reindeer')) heappush(h, (1, 'elf')) heappush(h, (3, 'sled')) heappop(h) # (1, 'elf')
-
itertools makes it simple to iterate through all combinations or permutations of a set:
k for k in permutations("🐉🥕😊") # ('🐉', '🥕', '😊'), ('🐉', '😊', '🥕'), ('🥕', '🐉', '😊'), ('🥕', '😊', '🐉'), ('😊', '🐉', '🥕'), ('😊', '🥕', '🐉')
. -
zip
can let you transpose a matrix (zip(*matrix)
) or pair two arrays (zip(a, b)
).zip
also makes it easy to pair up items from an array. Say you had an array of numbers[1, 2, 3, 4, 5, 6]
and wanted to multiply each pair before adding them:sum(a * b for a, b in zip(nums[::2], nums[1::2]))
.Similarly, you might want to see whether an array is in ascending order:
all(b > a for a, b in zip(nums, nums[1:]))
-
You can use True/False to index into arrays or to increment a counter. Should you do this in real code? No. Should you do this in Advent of Code code? Also, no.
for thing in things: counter[thing["type"]] += thing["field"] == "my_field"
-
Structural pattern matching is fantastic! Being able to write quick
case
statements about the shape of something leads to some tight code:def compare (left, right): match left, right: case int(), int(): return (left < right) - (right < left) case int(), list(): return compare([left], right) case list(), int(): return compare(left, [right]) case list(), list(): for result in map(compare, left, right): if result: return result return compare(len(left), len(right))
-
functools.cache
makes it simple to memoize a function:import functools @functools.cache def this_has_a_cache (a): return a + 1
Lessons from 2024
- Rather than using
re
, the parse library looks fantastic. It gives a much tighter syntax for pulling out values and uses{}
for capture groups rather than()
.()
are so common in things that I want to write regular expressions for! - You can union a
defaultdict
with a regular dictionary to easily set up a nice data structure for a problem:things = defaultdict(string) | { r + c * 1j: lines[r][c] for c in range(len(lines[r])) for r in range(len(lines))}