Skip to content

Latest commit

 

History

History
520 lines (367 loc) · 14.8 KB

caches.md

File metadata and controls

520 lines (367 loc) · 14.8 KB

cachex cache classes

cachex allows to extend mutable mapping types, including cachetools memoizing classes, to ease using them as caches.


Features

Apart from the features of mutable mapping types and cachetools memoizing classes, cachex classes provide several additional features.


Integrated locking capability

Access to caches is not thread-safe per se, but cache classes have by default an integrated lock that can be used to define exclusive access contexts.

cache = MyCache()
cache['a'] = 'Item a'
cache['b'] = 'Item b'

# Some thread spawning here.
...

with cache.lock:
    # Exclusive access to cache.
    try:
        print('Values: ', cache['a'], cache['b'])
    except KeyError:
        print('Missing value')

This integrated lock can be subtituted by any object that implements the context manager protocol, depending on the specific access exclusivity needs.

cache.lock = threading.RLock()
# or
cache.lock = threading.BoundedSemaphore()
# or
cache.lock = threading.Condition()
...

To reset to a default lock, assign True.

cache.lock = True

with cache.lock:
    # Exclusive access to cache.
    ...

To disable integrated locking, assign False.

cache.lock = False

# Context use will not break, so code does not need to be changed.
# It just will not acquire exclusive access.
with cache.lock:
    # Non exclusive access to cache.
    ...

assert(not cache.lock)

Integrated hit/miss counters

Cache classes have integrated support for counting hits and misses when accessing the cache.

Due to the duck typing nature of Python this cannot be completely automated, so the developer will have to decide where and how to use these counters. But several ways of easing this are provided.

  • Counters can be explicitly incremented specifying when a hit or a miss has occurred.

    cache = MyCache()
    cache['a'] = 'Item a'
    cache['b'] = 'Item b'
    
    for k in ('a', 'b', 'c'):
        try:
            print('Value of %r: %r' % (k, cache[k]))
            cache.did_hit()        # Explicitly increments hits.
        except KeyError:
            print('Missing value: %r' % k)
            cache.did_miss()       # Explicitly increments misses.
    
    assert(cache.hits == 2)
    assert(cache.misses == 1)
  • Alternatively, counters can be enabled to be incremented implicitly when accessing the cache.

    cache = MyCache()
    cache['a'] = 'Item a'
    cache['b'] = 'Item b'
    
    cache.counters_enabled = True
    
    for k in ('a', 'b', 'c'):
        try:
            print('Value of %r: %r' % (k, cache[k]))
        except KeyError:
            print('Missing value: %r' % k)
    
    cache.counters_enabled = False
    
    assert(cache.hits == 2)
    assert(cache.misses == 1)
  • The easiest way is to use the provided counters property to enclose a context where the counters will be incremented implicitly.

    cache = MyCache()
    cache['a'] = 'Item a'
    cache['b'] = 'Item b'
    
    with cache.counters:
    
        for k in ('a', 'b', 'c'):
            try:
                print('Value of %r: %r' % (k, cache[k]))
            except KeyError:
                print('Missing value: %r' % k)
    
    assert(cache.hits == 2)
    assert(cache.misses == 1)
    assert(not cache.counters_enabled)    # Still disabled outside the context.

    This context can also be used to specify where to NOT implicitly increment these counters.

    cache = MyCache()
    cache['a'] = 'Item a'
    cache['b'] = 'Item b'
    
    cache.counters_enabled = True
    
    print('Value of a: %r' % cache['a'])
    
    assert(cache.hits == 1)
    
    with cache.counters(False):
    
        print('Cache contents:')
        for k in cache:
            # Hits will not be automatically incremented.
            print('Value of %r: %r' % (k, cache[k]))
    
    assert(cache.hits == 1)
    assert(cache.counters_enabled)    # Still enabled outside the context.

The counters property can also be used to check if the hit/miss counters are enabled or have ever been incremented, so you can discriminate when to have these counters into account or not at a later point.

if cache.counters:
    print('hits: %r, misses: %r' % (cache.hits, cache.misses))
else:
    print('Cache counters not in use')

As seen in the examples, the number of hits and misses registered by the cache can be obtained using he hits and misses properties.

The hit/miss counters can be reset at any time.

cache.counters_reset()
# or
cache.clear()      # This also clears the cache.

Just as a convenience, the counters property can also be used as an alternative access to reset the hit/miss counters or to enable/disable its implicit use.

cache.counters.reset()
# is equivalent to
cache.counters_reset()
cache.counters.enabled
# is equivalent to
cache.counters_enabled

Cache information

The parameters property provides a dictionary with the parameters used to create the cache, including the unspecified ones that took default values. This is for information purposes only, changing the values has no effect.

The info property provides a named tuple showing hits, misses, maximum size and current size. This helps measuring the efectiveness of the cache and helps tuning its parameters.

Note that hits and misses infomation only has sense if the integrated cache counters are used.

>>> cache = MyCache(ttl=3600)
>>> 
>>> cache.parameters
{'maxsize': 128, 'ttl': 3600}
>>> 
>>> cache['a'] = 'Item a'
>>> cache.info
CacheInfo(hits=0, misses=0, maxsize=128, currsize=1)

Each cache instance also provides a unique hash value. This value does not contain any information about the cache contents.

cache1 = MyCache()
cache2 = MyCache()

assert(hash(cache1) != hash(cache2))

Cloning capability

A cache instance can be cloned to get an empty fresh copy of it.

cache1 = MyCache()
cache1['a'] = 'Item a'

cache2 = cache1.clone()

assert(cache1.currsize == 1)
assert(cache2.currsize == 0)

assert(type(cache1) == type(cache2))
assert(cache1.parameters == cache2.parameters)

Conversion of cache classes

Any mutable mapping type, including cachetools cache classes, can be converted into cachex cache classes acquiring all its features while fully preserving backward compatibility.

>>> from cachex import caches
>>> from cachetools import Cache     # -> cachetools cache class.
>>> 
>>> Cache = caches.convert(Cache)    # -> cachex cache class.
>>> 
>>> cache = Cache()
>>> cache.info
CacheInfo(hits=0, misses=0, maxsize=128, currsize=0)

Even plain dict and weakref.WeakValueDictionary types can be converted.

>>> from cachex import caches
>>> 
>>> DictCache = caches.convert(dict)
>>> 
>>> cache = DictCache()
>>> cache.info
CacheInfo(hits=0, misses=0, maxsize=None, currsize=0)

If you try to convert an already converted class it will not be converted again, you will just get the same class.

Note: The conversion consists in providing a wrapper class around the original mutable mapping type. For this reason it is not recommended to develop cache classes inherited from cachex classes (if you want to use features like managed defaults or cloning, for example). The prefered method to develop a cache class with cachex features is to inherit from a mutable mapping (for example a cachetools cache class) and then convert the developed class.


Pools of cache classes

Pools are containers of cache classes that you can organize to conveniently access all your converted cache classes from all over your application.

Meet the cachex main pool of cache classes. Some already implemented cache classes are provided by default in the main pool of classes.

from cachex import caches

cache = caches.UnboundedCache()

For convenience, any cache class can be added to a pool of classes. This makes the class accessible across modules without having to import or convert it in each module you use it.

If a standard mutable mapping type (for example a cachetools cache class) is added to a pool, it will be automatically converted.

Example:

from cachex import caches
from mycaches import MyCache     # -> Mutable mapping class.

caches.add(MyCache)

Now the class can be used in other modules without having to import or convert it again.

from cachex import caches

cache = caches.MyCache()
cache.info

Even entire modules containing mutable mapping classes can be added to the pool to have those classes conveniently converted and accessible.

from cachex import caches
import mycaches          # -> Module with mutable mapping classes.

caches.add(mycaches)

cache = caches.mycaches.MyCache()

Alternative names can be used to avoid collisions.

# Adding a cache class with alternative name.
caches.add(MyCache, 'OtherCache')

cache = caches.OtherCache()
# Adding a module with alternative name.
caches.add(mycaches, 'othercaches')

cache = caches.othercaches.MyCache()

When adding a module, the created container is also another pool of classes.

Examples:

from cachex import caches

import mycaches
import othercaches
import nestedcaches
from othermodule import MyCache

caches.add(mycaches)
caches.mycaches.add(MyCache)
otherpool = caches.add(othercaches, 'other')
otherpool.add(nestedcaches, 'nested')

assert(caches.mycaches.MyCache)
assert(caches.other.nested)

Empty pools can also be added or created to use them at will, set global defaults, etc. Any structure can be built to conveniently access cache classes.

caches.add('emptypool')
assert(caches.emptypool)

mypool = caches.add()
assert(mypool.defaults)

Defaults management

Default parameters for creating cache instances are centralized, global and can be easily accessed and modified using any pool of cache classes.

>>> from cachex import caches
>>> cache1 = caches.MyCache()
>>> cache1.maxsize
128
>>> caches.defaults.maxsize = 1024
>>> cache2 = caches.MyCache()
>>> cache2.maxsize
1024

Note: The defaults are global by design. Modifying them will affect the defaults for all cache classes in all pools.

You can create defaults for your own developed cache classes.

Example:

# Create a customized cache class with customized constructor arguments.

import cachetools

class MyCache(cachetools.Cache):

    def __init__(self, maxsize, custom_param):
        self.custom = custom_param
        super().__init__(maxsize=maxsize)

# Add to the pool of caches, for convenience.
from cachex import caches
caches.add(MyCache)

# Set a default value for your customized argument.
caches.defaults.custom_param = 'CUSTOM VALUE'

Now the class can be instantiated in any module using the default value specified for the parameter.

from cachex import caches

cache = caches.MyCache()    # No parameters informed.
print(cache.custom)         # -> 'CUSTOM VALUE'

Defaults can also be defined and accessed by name.

caches.defaults['custom_param'] = 'CUSTOM VALUE'

Defaults can even be deleted, if you want to get rid of a specific one and force to inform it on object creation.

del caches.defaults.custom_param
# or
del caches.defaults['custom_param']

For any given parameter, there is the possibility to define a different default value to use when the parameter is not missing but explicitly set to None. If this is not set, the default value for missing parameter will be used for both cases.

# Default value for missing parameter.
caches.defaults.custom_param

# If set, default for explicit None value.
caches.defaults.custom_param__None

You can check the defined defaults at any moment.

>>> from cachex import caches
>>> caches.defaults
CacheDefaults({'maxsize': 128, 'ttl': 600})
>>> 'maxsize' in caches.defaults
True
>>> list(caches.defaults)
['maxsize', 'ttl']
>>> dict(caches.defaults)
{'maxsize': 128, 'ttl': 600}

There are also some protected defaults. Protected defaults are not shown when defaults are listed but can be modified anyway. Modify them only if you know what you are doing.

# Default lock class or factory for cache objects.
caches.defaults._lock_class

Cache implementations

Some cache classes are provided by default in the main pool of classes.

Please refer to the cachetools documentation for a better understanding of the caches parameters.

  • A direct conversion of the classes provided by cachetools.

    These are documented in the cachetools documentation.

  • class caches.UnboundedCache(getsizeof=None)

    Unbounded cache that never evicts items and can grow without limit.

  • class caches.UnboundedTTLCache(ttl, timer=time.monotonic, getsizeof=None)

    Cache without size limit that evicts items only when their time-to-live expires, and not based on the cache size.

    Refer to the cachetools documentation for details on the parameters.

  • class caches.NoCache(getsizeof=None)

    Implementation of a zero-size cache that does not store items and thus always misses. Not intended to be useful for production use, just for testing and development purposes.

As stated in the section about converting classes, it is not recommended to develop cache classes inherited from cachex classes. The developed classes must inherit from a mutable mapping type (like cachetools cache classes), and later be converted and optionally added to a pool.

For this purpose, standard versions of these mutable mapping classes are made available.

from cachex import standard, caches
standard.UnboundedTTLCache    # -> Standard mutable mapping class
caches.UnboundedTTLCache      # -> cachex version