Okay, I just got a couple of spare hours to play around with the available prior art. Again, thanks all for the various pointers!
@patrick-kidger’s version was a good place for me to start hacking around, but I think inspect usage is a no-go: as @DavidCEllis noted it’s a performance killer. My current preference, of the various versions I’ve tried, would be for the following:
def _make_callback(cache_dict, id_key):
def callback(ref):
cache_dict.pop(id_key)
return callback
def _make_cached_func(ref, unbound_method, maxsize, typed):
@functools.lru_cache(maxsize, typed)
def wrapped(*args, **kwargs):
return unbound_method(ref(), *args, **kwargs)
return wrapped
def cached_method(maxsize=None, typed=False):
def decorator(func):
per_instance_cache = {}
@functools.wraps(func)
def wrapper(self, *args, **kwargs):
self_id = id(self)
try:
ref, func_using_ref = per_instance_cache[self_id]
except KeyError:
ref = weakref.ref(self, _make_callback(per_instance_cache, self_id))
func_using_ref = _make_cached_func(ref, func, maxsize, typed)
per_instance_cache[self_id] = ref, func_using_ref
return func_using_ref(*args, **kwargs)
return wrapper
return decorator
(I’ve omitted types, as a matter for typeshed rather than stdlib.)
This works on slotted classes just fine, as long as they are weak-reference-able. Hashability is not required, and the cache does not get pickled (I consider this good).
Because this operates on the unbound function, not the bound method, it’s simpler than several other versions. Keying by id but mapping this back to a weakref ensures that each instance gets a unique cache – having had additional time to consider it, that seems to me like necessary behavior to avoid confusing people.
There are only two issues I could find. (1) it’s a little slower than I’d like, taking around 100ns of overhead on my machine to use a cached_method vs a similar lru_cache-wrapped function. (2) we don’t have access to cache_clear() or cache_info() for the method – providing this would require a descriptor.
The biggest missing ingredient here is buy-in from the core-devs who look after functools.
After or during PyCon, I may try to figure out how to get a clearer idea of what they would consider the minimum requirements for an implementation of functools.cached_method.