Source code for zfit.util.cache
"""Module for caching.
The basic concept of caching in Zfit builds on a "cacher", that caches a certain value and that
is dependent of "cache_dependents". By implementing `ZfitCachable`, an object will be able to play both
roles. And most importantly, it has a `_cache` dict, that contains all the cache.
Basic principle
===============
A "cacher" adds any dependents that it may comes across with `add_cache_dependents`. For example,
for a loss this would be all pdfs and data. Since :py:class:`~zfit.Space` is immutable, there is no need to add this
as a dependent. This leads to the "cache_dependent" to register the "cacher" and to remember it.
In case, any "cache_dependent" changes in a way the cache of itself (and any "cacher") is invalid,
which is done in the simplest case by decorating a method with `@invalidates_cache`, the "cache_dependent":
* clears it's own cache with `reset_cache_self` and
* "clears" any "cacher"s cache with `reset_cache(reseter=self)`, telling the "cacher" that it should
reset the cache. This is also where more fine-grained control (depending on which "cache_dependent"
calls `reset_cache`) can be brought into play.
Example with a pdf that caches the normalization:
.. code:: python
class Parameter(Cachable):
def load(new_value): # does not require to build a new graph
# do something
@invalidates_cache
def change_limits(new_limits): # requires to build a new graph (as an example)
# do something
# create param1, param2 from `Parameter`
class MyPDF(Cachable):
def __init__(self, param1, param2):
self.add_cache_dependents([param1, param2])
def cached_func(...):
if self._cache.get('my_name') is None:
result = ... # calculations here
self._cache['my_name']
else:
result = self._cache['my_name']
return result
"""
# Copyright (c) 2019 zfit
import abc
from abc import abstractmethod
import functools
from zfit.util import ztyping
from zfit.util.container import convert_to_container
[docs]class ZfitCachable:
[docs] @abstractmethod
def register_cacher(self, cacher: "ZfitCachable"):
raise NotImplementedError
[docs] @abstractmethod
def add_cache_dependents(self, cache_dependents, allow_non_cachable):
"""Add dependents that render the cache invalid if they change.
Args:
cache_dependents (ZfitCachable):
allow_non_cachable (bool): If `True`, allow `cache_dependents` to be non-cachables.
If `False`, any `cache_dependents` that is not a `ZfitCachable` will raise an error.
Raises:
TypeError: if one of the `cache_dependents` is not a `ZfitCachable` _and_ `allow_non_cachable`
if `False`.
"""
pass
[docs] @abstractmethod
def reset_cache_self(self):
"""Clear the cache of self and all dependent cachers."""
pass
[docs] @abstractmethod
def reset_cache(self, reseter):
pass
[docs]class Cachable(ZfitCachable):
def __init__(self, *args, **kwargs):
self._cache = {}
self._cachers = {}
self.reset_cache_self()
super().__init__(*args, **kwargs)
[docs] def register_cacher(self, cacher: ztyping.CacherOrCachersType):
"""Register a `cacher` that caches values produces by this instance; a dependent.
Args:
cacher ():
"""
if not isinstance(cacher, ZfitCachable):
raise TypeError("`cacher` is not a `ZfitCachable` but {}".format(type(cacher)))
if not cacher in self._cachers:
self._cachers[cacher] = None # could we have a more useful value?
[docs] def add_cache_dependents(self, cache_dependents: ztyping.CacherOrCachersType,
allow_non_cachable: bool = True):
"""Add dependents that render the cache invalid if they change.
Args:
cache_dependents (ZfitCachable):
allow_non_cachable (bool): If `True`, allow `cache_dependents` to be non-cachables.
If `False`, any `cache_dependents` that is not a `ZfitCachable` will raise an error.
Raises:
TypeError: if one of the `cache_dependents` is not a `ZfitCachable` _and_ `allow_non_cachable`
if `False`.
"""
cache_dependents = convert_to_container(cache_dependents)
for cache_dependent in cache_dependents:
if isinstance(cache_dependent, ZfitCachable):
cache_dependent.register_cacher(self)
elif not allow_non_cachable:
raise TypeError("cache_dependent {} is not a `ZfitCachable` but {}".format(cache_dependent,
type(cache_dependent)))
[docs] def reset_cache_self(self):
"""Clear the cache of self and all dependent cachers."""
self._clean_cache()
self._inform_cachers()
[docs] def reset_cache(self, reseter: 'ZfitCachable'):
self.reset_cache_self()
def _clean_cache(self):
self._cache = {}
def _inform_cachers(self):
for cacher in self._cachers:
cacher.reset_cache(reseter=self)
[docs]def invalidates_cache(func):
@functools.wraps(func)
def wrapped_func(*args, **kwargs):
self = args[0]
if not isinstance(self, ZfitCachable):
raise TypeError("Decorator can only be used in a subclass of `ZfitCachable`")
self.reset_cache(reseter=self)
return func(*args, **kwargs)
return wrapped_func