How to cache a method of an unhashable type in Python?

3 min read Original article ↗

Steven Van Ingelgem

Press enter or click to view image in full size

Introduction

We’ve been all there… The infamous unhashable type error. Yeah, I know a list is not hashable (and if you don’t know yet, you could for example read this article).
This article is not about this issue. The issue at hand is that I know I have a non-frozen type where I have a bound method that I would very much like to be cached.

So, first some code:

class ConfigList(list):
def matches(self, txt: str) -> bool:
for el in self:
if txt in el:
return True

return False

config_list = ConfigList(['abc', 'def'])
print('ab:', config_list.matches('ab'))
print('defg:', config_list.matches('defg'))

The result:

ab: True
defg: False

This class provides an extra method to see if the passed value is part of any element of the list. To me, I needed to do something similar but much more resource intensive and like 1_000’s of times a minute. So, I needed a cache.

Adding @lru_cache

class ConfigList(list):
@lru_cache
def matches(self, txt: str) -> bool:
...

But this didn’t work as intended:

Traceback (most recent call last):
File "dummy.py", line 15, in <module>
print('ab:', config_list.matches('ab'))
TypeError: unhashable type: 'ConfigList'

I know that ConfigList is not hashable, but in the lru_wrapper code the keys are taken over ALL arguments, of which self is one too… Hence the TypeError .

Get Steven Van Ingelgem’s stories in your inbox

Join Medium for free to get updates from this writer.

How to fix this for bound methods?

Option 1: methodtools

Link: https://pypi.org/project/methodtools/

You don’t need to change anything just import from methodtools instead of functools:

from methodtools import lru_cache

The only con I see here is that you need to cache the class/staticmethod instead of the way around as I would suspect it.

Option 2: create your own method

Credits go to orlp on SO: https://stackoverflow.com/a/33672499/577669

import functools
import weakref

def lru_cache(maxsize=128, typed=False):
def decorator(func):
@functools.wraps(func)
def wrapped_func(self, *args, **kwargs):
self_weak = weakref.ref(self)
@functools.wraps(func)
@functools.lru_cache(maxsize=maxsize, typed=typed)
def cached_method(*args, **kwargs):
return func(self_weak(), *args, **kwargs)
setattr(self, func.__name__, cached_method)
return cached_method(*args, **kwargs)
return wrapped_func

if callable(maxsize) and isinstance(typed, bool):
# The user_function was passed in directly via the maxsize argument
func, maxsize = maxsize, 128
return decorator(func)

return decorator

What this is basically doing is to create a method under the hood which only have the arguments. That special one is cached. And the self is referenced weakly. If you would not do this the self will never be able to be cleaned up when the instance is killed as the circular references in the closure stays up.

I don’t see any cons for using this method.

Conclusion

I do like the own method more because I can use it directly on the method, not above the classmethod/staticmethod as methodtools says you have to do.

But please do let me know in the comments if you have any way to tackle this problem!