Package: epic-caching
Epic caching - Utilities for cachingWhat is it?Theepic-cachingPython library provides several utilities which help in getting previously
calculated values or previously created objects. It has some overlap with the standard library@functools.cacheand@functools.cached_property, albeit with some additional functionality.UsageThe utilities in the library allow for three types of functionalities:Declaring a class as a SingletonCaching the results of a function or the instances of a class by arguments, either on a
single-call basis or always.Adding flexible cached properties to classes.Make a class into a SingletonTheSingletonmetaclass transforms a class into a singleton:fromepic.cachingimportSingletonclassMySingleton(metaclass=Singleton):...This means that there exists at-most
a single instance of the class. On first instantiation, the initialization parameters are used, and
on additional calls, they are ignored and the single instance is returned.Cache class instances or the results of function callsWhile theSingletonmetaclass ensures a single instance, sometimes we need the same instance
to be created if the same initialization arguments are given, but a different instance for different arguments.
This behavior is similar to what@functools.cachedoes. The library extends this functionality in two ways:It allows some calls to the function or class to be cached but not others.It can cache the results in one of two "scopes", either a single cache shared by all threads, or
a separate cache for each thread.To only cache a call to a callable object, use thecached_callfunction. It receives the*argsand**kwargsto send to the callable, as well as an optionalscopeparameter (either"process"or"thread"),
specifying the scope of the cache:importrandomfromepic.cachingimportcached_calldeffunc(x):returnx+random.random()assertcached_call(func,3)iscached_call(func,3)To cache all calls to a function, use thecacheddecorator, which also receives an optionalscope:importrandomfromepic.cachingimportcached@cacheddeffunc(x):returnx+random.random()assertfunc(4)isfunc(4)If instead we used@cached(scope='thread'), repeated calls with the same arguments within the same thread would
return the same object, but not in different threads.To cache all instances of a class, use theCachedmetaclass:fromepic.cachingimportCachedclassA(metaclass=Cached):def__init__(self,x):self.x=xassertA(5)isA(5)assertA(6)isnotA(5)Had we used theSingletonmetaclass, the second assertion would have failed, since there would be only a single
instance ofA. With theCachedmetaclass, the same arguments return the same instance, but different arguments
return different instances.TheCachedmetaclass can also be given ascope:fromepic.cachingimportCachedclassB(metaclass=Cached,scope='thread'):...The behavior is similar to those ofcached_callandcached.Cached propertiesThe library provides acached_propertydecorator, which is very similar to the standard one fromfunctools.
However, it provides some functionalities absent from the standard decorator:It is possible to provide a set ofdependenciesfor the property. These are the names of members on which
the value of the property depends. When the cached property is accessed, these members are accessed. If their
values have changed since last access, the property is recalculated. If not, the cached value is returned.
Note that, contrary to the caching mechanisms described above, we do not cache multiple values for the property
(one for each set of dependency values). Only a single value is cached at any given moment. If the dependencies
change, the new value is calculated and stored in the cache, replacing of the old one.The cached property can also be set and deleted. When the property is set, the given value is also stored in
the cache. When it is deleted, the cache is cleared.The cached property can be used by multiple threads. The cache is shared by all threads and the operations
on the property are thread safe.The usage of the property is trivial:fromepic.cachingimportcached_propertyclassClassWithCachedProp:@cached_propertydefprop(self):returnself.expensive_computation()instance=ClassWithCachedProp()assertinstance.propisinstance.propinstance.prop=3.14delinstance.propIn the line beginning withassert, the expensive computation is carried out only once.To specify dependencies, provide the member names as strings to thecached_propertyconstructor:fromepic.cachingimportcached_propertyclassAnotherClassWithCachedProp:def__init__(self,x):self.x=x@cached_property('x')defprop(self):returnself.x+self.expensive_computation()By declaring this dependency, now every timeself.propis accessed, if the value ofself.xhas changed,propwould need to be recalculated.To check explicitly whether the stored dependency values match the current member values, use the methodis_cache_valid. Note that in order to do so, the property must be accessed using the host class, not its instances:instance=AnotherClassWithCachedProp(42)ifAnotherClassWithCachedProp.prop.is_cache_valid(instance):...SerializationIt is often desired to serialize objects without their stored cache values. The reason is usually to save space,
since the stored values do not constitute "part of the object", just an optimization implementation. In order to
remove the cache when serializing, we need to define the__getstate__method. In it, we can get the state of the
object without its cache using thestate_without_cachestatic method:fromepic.cachingimportcached_propertyclassPersistent:# cached_property is used at least once in the class definition...def__getstate__(self):returncached_property.state_without_cache(self)The returned state is the object's__dict__, or the result of its base class__getstate__method, if it exists.
If the state is a dictionary, it is copied and the cache is removed from the copy. Of course, if other manipulations
on the state should be performed during serialization, they can be done so in__getstate__on the returned value
before returning it.Secondary cache in a fileSometimes it can be useful to have the value of the property persist between runs of the program, or between processes.
To do so, we can automatically save a cached property value in a file. This file would hold the (pickled) value of
a single property. Generally, this makes sense only if we plan to have only a single instance of the class,
as multiple instances would share the same cache file. Note that the pickle file acts as asecondarycache, in
addition to the cache in memory: if the value cannot be found in the memory cache, it is searched for in the file.
When a value is cached, it is saved both in memory and in file.To define a cached property with a secondary file cache, use thepickled_cached_propertydecorator. It is a
subclass ofcached_propertythat, in addition to optional dependencies, must get afilenameparameter:fromepic.cachingimportpickled_cached_propertyclassPersistsToFile:def__init__(self,x):self.x=x@pickled_cached_property('x',filename='prop_cache.pkl')defprop(self):returnself.x+self.expensive_computation()Of course, as a subclass ofcached_property, the pickled property can also be set or deleted.If there are dependencies, they can be incorporated into the filename, using theformatsyntax (i.e.{...}),
resulting in the creation of different files for different values of the dependencies:fromepic.cachingimportpickled_cached_propertyclassPersistsToFile:def__init__(self,x,workdir='.'):self.x=xself.workdir=workdir@pickled_cached_property('x','workdir',filename='{workdir}/prop_cache_for_{x}.pkl')defprop(self):returnself.x+self.expensive_computation()Lazy propertiesA lazy property is a cached property that should only be calculated when all of its dependencies are "set".
In this context, a member is considered "set" when it exists and its value is notNone. Until all the dependencies
are set, the value of the lazy property itself isNone:fromepic.cachingimportlazy_propertyclassLazy:@lazy_property('x')defy(self):returnself.x+1lazy=Lazy()assertlazy.yisNonelazy.x=1assertlazy.yis2Thelazy_propertydecorator is a subclass ofcached_property. Note that without any dependencies, a lazy
property is no different from a cached property. Of course, after all the dependencies are set and the lazy property
is calculated, its value is cached as usual.