@cache without @cache
Encapsulating dp[] within functions
Memoization is a part of the standard toolkit for, “things I can use to solve the algorithm question in my next job interview”. Most of the time, I like to use functools.cache
for this:
Using the fibonacci function as an example, a simple Python implementation might look like this:
|
|
However, some interviewers might reject that One Weird Trick, given that the point of most coding interviews is to check for one’s understanding of the algorithm, rather than its existence.
So, instead of importing a solution, you’d manually create/check a hashtable, typically named dp[]
:
|
|
And this works just as well as the @cache
solution:
|
|
Job well done? Probably.
But personally, don’t like it. The @cache
d solution is simple, readable, and intuitive. It’s practically identical to how you’d write it mathematically:
$$ \forall\ n > 1, F_n = F_{n-1} + F_{n-2}$$
The manual dp[]
solution is not as nice. The base cases are a bit more obvious, but the meat of the function is a lot more different. Plus, the dp[]
variable gets leaked to the global namespace, when it should really only be accessible to the fib()
function itself. Can we do better?
Alternatives
I want to bind a stateful variable (dp[]
) to a function. The natural tool for that in python is an object, so let’s do that:
|
|
This works (as in, dp{}
is no longer a global variable), but it’s much uglier. Can we do better?
Did you know that in Python, functions are objects too?
|
|
You can assign any attribute to a function, and it’ll just work. Albeit with potential performance issues.
Still, both of these options leaves .dp
available to the public namespace. Can we hide it entirely?
In theory, this is what you’d use a closure for:
|
|
But this is really long and ugly. We can separate the business logic with the decorator pattern:
|
|
And, in doing so, return to the original @cache
design for the fib()
function. This is even somewhat similar to what @cache itself does, the Least Recently Used part aside.
The bootleg @dpcache
solution might be the cleanest, but it also has the largest number of lines of all the solutions suggested. If we’re comfortable with making our code look ugly, we can use a hack with default variables instead:
|
|
This is very close to the essence of our initial design for manual memoization, while also keeping the dp[]
dictionary encapsulated. The positional glob (*
) limits the potential for an accidental fib(n, ?)
call, but it’s possible someone else might unwittingly add the dp=
argument after reading fib()
’s type signature.
The use of default arguments in a singleton fashion this way is also, practically speaking, too smart for its own good – it’s a Code Smell, albeit in a different manner than having global variables.