January 7, 2023

Miscellaneous Python Bits

In a previous post I said I would come back to explain a few somewhat opaque lines in my matrix-based solution to Advent of Code 2022, day 2. This is what I’m going to do here.

There’s three smallish things I thought it was worth pointing out. None is going to surprise veteran python coders, but they might be of interest to some.

Building matrices

So the first line I want to look at is:

B = np.array(list("012" * 3)).astype(int).reshape(3, 3)

This builds the matrix $B$ from the previous post as a numpy array. Reminder that matrix $B$ looks like this:

$$ B = \begin{pmatrix} 0 & 1 & 2 \\ 0 & 1 & 2 \\ 0 & 1 & 2 \\ \end{pmatrix} $$

So this line of code is throwing together a bunch of small things that we should unpack. First, in the middle there, we have a string "123", which we then multiply by 3. Multiplying a string by $n$ means repeating it $n$ times. So "012" * 3 is the same as "012012012". Next we turn that string into a list. That is, we treat the string as a list of single character strings: list("012" * 3) is: ["0", "1", "2", "0", "1", "2", "0", "1", "2"].

We take that list, and turn it into a numpy array. np.array(list("012"*3)) is a one-dimensional array of strings.

>>> np.array(list("012"*3))
array(['0', '1', '2', '0', '1', '2', '0', '1', '2'], dtype='<U1')

That dtype='<U1' is telling us that numpy is treating the data type of the array as unicode strings, which is not what we want. .astype(int) turns it into a one-dimensional array of ints (which works because each string can be cast to an int).

>>> np.array(list("012"*3)).astype(int)
array([0, 1, 2, 0, 1, 2, 0, 1, 2])
>>> np.array(list("012"*3)).astype(int).dtype

And finally, .reshape(3,3) reshapes the array into a 3 by 3 array.

>>> np.array(list("012" * 3)).astype(int).reshape(3, 3)
array([[0, 1, 2],
       [0, 1, 2],
       [0, 1, 2]])

Matching vectors

The second thing I wanted to explain is _get_vec:

def _get_vec(in_val, values):
    return (np.array(list(values)) == in_val).astype(int)

Recall that for the AoC problem, the input was a string, with moves represented by letters – “A”, “B” or “C” for their move, “X”, “Y”, or “Z” for my move – and what we wanted to get out, for the matrix solution was a unit vector with a “1” in the position corresponding to the move. That is, if their move was a “B”, we wanted to get out $(0,1,0)$. This is precisely what the above function does: put in “B” and “ABC” as the two arguments and it returns np.array([0,1,0]).

So what is it doing? Starting from the inside again, we have list(values) which is doing the same trick we had before: take a string, and return a list of characters. Then we turn that list into a numpy array. We then compare that numpy array of characters to the in_val argument, using the == comparison operator. This gives us a boolean array: with False where the character in that position doesn’t match in_val and True where we do have a match. So np.array(list("ABC")) == "B" is np.array([False, True, False]). The final step is to turn that boolean array into an array of integers: False maps to 0 and True maps to 1. This gives us np.array([0,1,0]) as desired.

Matrix multiplication

The last thing I wanted to mention is the quickest, and possibly the most surprising. Python has a matrix multiplication infix operator! Or rather, there’s a PEP spec for one, and numpy implements it. Use A @ B to multiply matrices A and B together. That’s neat, and not something I was aware of until I went looking for it. Will I get much use out of it? Probably not. But it’s nice to know it’s there.

© Seamus Bradley 2021–3

Powered by Hugo & Kiss.