Source code for gammapy.utils.cache
# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""Utilities for caching"""
import functools
import inspect
import hashlib
import weakref
from gammapy.utils.parallel import is_ray_available
USE_INSTANCE_CACHE = False
if is_ray_available():
import ray
def _hash(value):
try:
return hash(value)
except TypeError:
data = ray.cloudpickle.dumps(value)
return hashlib.sha256(data).hexdigest()
else:
def _hash(value):
return hash(value)
[docs]
def make_key(sig, *args, **kwargs):
"""
Generate a unique hash key for caching based on normalized constructor arguments.
This function uses the signature of the class constructor (`__init__`) to normalize
the input arguments, serializes them using `ray.cloudpickle`, and returns a SHA-256 hash
digest. The resulting key is suitable for use in caching mechanisms.
Parameters
----------
sig : inspect.Signature
The signature of the method or function used to normalize arguments.
*args : tuple
Positional arguments to be normalized.
**kwargs : dict
Keyword arguments to be normalized.
Returns
-------
key : str
A SHA-256 hexadecimal digest representing the normalized arguments.
"""
bound = sig.bind_partial(None, *args, **kwargs)
bound.apply_defaults()
normalized_args = dict(sorted(bound.arguments.items()))
normalized_args.pop("self", None)
return _hash(tuple(normalized_args.items()))
class _WeakIdDict:
"""Like `weakref.WeakKeyDictionary`, but uses identity-based hashing and equality.
from https://github.com/python/cpython/issues/102618#issuecomment-2839489762.
"""
__slots__ = ("_dict",)
def __init__(self):
self._dict = {}
def __getitem__(self, key):
item, _ = self._dict[id(key)]
return item
def __setitem__(self, key, value):
id_key = id(key)
ref = weakref.ref(key, lambda _: self._dict.pop(id_key))
self._dict[id_key] = (value, ref)
[docs]
def cachemethod(fn):
"""
Decorator to cache method results on a per-instance basis using weak references.
This decorator stores cached results of method calls in a `WeakKeyDictionary`,
ensuring that the cache is automatically cleared when the instance is garbage collected.
The cache key is generated using `make_key`, which normalizes and hashes the method
arguments to ensure consistent and order-independent caching.
Parameters
----------
fn : callable
The method to be decorated.
Returns
-------
wrapper : callable
The wrapped method with caching behavior.
"""
cache1 = _WeakIdDict()
sig = inspect.signature(fn)
parameters = list(sig.parameters.values())
if len(parameters) == 0 or parameters[0].kind not in {
inspect.Parameter.POSITIONAL_ONLY,
inspect.Parameter.POSITIONAL_OR_KEYWORD,
}:
raise ValueError(
"The `@cachemethod` decorator can only be used on instance methods "
"with a positional `self` argument."
)
@functools.wraps(fn)
def wrapper(self, *args, **kwargs):
argkey = make_key(sig, *args, **kwargs)
try:
out = cache1[self][argkey]
except KeyError:
try:
cache2 = cache1[self]
except KeyError:
cache2 = cache1[self] = {}
out = cache2[argkey] = fn(self, *args, **kwargs)
return out
return wrapper
[docs]
class CacheInstanceMixin:
"""Cache class instance based on init arguments equivalence"""
[docs]
def __new__(cls, *args, **kwargs):
new = super().__new__(cls)
if not USE_INSTANCE_CACHE or not is_ray_available():
return new
# Ensure each subclass has its own cache
if not hasattr(cls, "_instances"):
cls._instances = _WeakIdDict()
try:
new.__init__(*args, **kwargs)
except TypeError:
return new # ignore cache for unpickle
sig = inspect.signature(cls.__init__)
argkey = make_key(sig, *args, **kwargs)
try:
out = cls._instances[cls][argkey]
except KeyError:
try:
cache2 = cls._instances[cls]
except KeyError:
cache2 = cls._instances[cls] = {}
out = cache2[argkey] = new
return out