API Reference for @cached() Decorator¶
Overview¶
The cached decorator provides a caching mechanism that stores the result of a function based on a specified key, time-to-live (TTL), and optional encoding/decoding functions. It can be applied to both synchronous and asynchronous functions, facilitating quick access to previously computed results. This includes respecting a configurable default_cache_ttl when no explicit ttl is provided.
Function: cached¶
Description¶
The cached decorator caches the results of a function execution using a unique key. If the function is called again with the same key before the TTL expires, the cached result is returned instead of re-executing the function. This is particularly useful for expensive computations or IO-bound tasks.
There are two main ways to use caching with py-cachify:
- Via the global
cacheddecorator exported frompy_cachify, which relies on a globally initialized client. - Via instance-based decorators obtained from a
Cachifyobject created byinit_cachify(is_global=False).
Parameters¶
| Parameter | Type | Description |
|---|---|---|
key |
str |
The key used to identify the cached result, which can utilize formatted strings to create dynamic keys. (i.e. key='my_key-{func_arg}') |
ttl |
Union[int, None], optional |
Time-to-live (seconds) for the cached result. If omitted, the decorator uses the cache client's default_cache_ttl (configured via init_cachify). If ttl is None, the value is stored without expiration. If ttl is an integer, that value is used directly and overrides any default_cache_ttl. |
enc_dec |
Union[Tuple[Encoder, Decoder], None], optional |
A tuple containing the encoding and decoding functions for the cached value. Defaults to None, which means that no encoding or decoding functions will be applied. |
Default TTL behavior¶
The effective TTL for a cached value is determined as follows (higher items take precedence over lower ones):
- If you pass an explicit integer, for example
@cached(..., ttl=30), that TTL is used and overrides anydefault_cache_ttl. - If you pass
ttl=None, the cache entry is stored without expiration (infinite TTL in most backends), even ifdefault_cache_ttlis configured. - If you omit
ttlentirely, the decorator will fall back to the underlying client'sdefault_cache_ttl: default_cache_ttlis configured viainit_cachify(default_cache_ttl=...)for both global and instance-based usage.- If
default_cache_ttlisNone(the default), omittingttlbehaves like “no expiration”.
This lets you define a global or instance-specific default TTL once and only override it where needed. When you do not configure default_cache_ttl at all (leaving it as None) and also omit ttl on the decorator, the behavior is the same as in previous versions of py-cachify: cached values are stored without expiration by default.
Returns¶
WrappedFunctionReset: A wrapped function (either synchronous or asynchronous) with an additionalresetmethod attached for cache management. Thereset(*args, **kwargs)method allows the user to manually reset the cache for the function using the same key.
Method Behavior¶
-
For Synchronous Functions:
- Checks if a cached value exists for the provided key.
- If the cached value exists, it returns the decoded value.
- If not, it executes the function, caches the result (after encoding, if specified), and then returns the result.
-
For Asynchronous Functions:
- Similar checks are performed in an asynchronous context using
await. - The caching behavior mirrors the synchronous version.
- Similar checks are performed in an asynchronous context using
Global Usage Example¶
from py_cachify import cached, init_cachify
# Configure a default cache TTL of 60 seconds for all cached values
init_cachify(default_cache_ttl=60)
@cached('my_cache_key')
def compute_expensive_operation(param: int) -> int:
# Uses default_cache_ttl=60 as TTL
return param * 2
@cached('my_async_cache_key-{param}', ttl=30)
async def fetch_data(param: int) -> dict:
# Overrides the default and uses ttl=30
return {'data': param}
Instance-based Usage¶
If you need multiple independent caches (for example, per module or subsystem), you can create dedicated Cachify instances via init_cachify(is_global=False) and use their cached method instead of the global decorator.
from py_cachify import init_cachify
# Create a dedicated instance that does not affect the global client
# and set a default TTL of 300 seconds for this instance
local_cachify = init_cachify(is_global=False, prefix='LOCAL-', default_cache_ttl=300)
@local_cachify.cached(key='local-{x}-{y}')
def local_sum(x: int, y: int) -> int:
# Uses the instance-level default_cache_ttl=300
return x + y
@cached(...)(global) uses the client configured by a globalinit_cachify()call.@local_cachify.cached(...)uses a client that is completely independent from the global one.
Multi-layer Usage¶
It is possible to layer caches by stacking cached decorators (for example, a global cache inside a local instance cache).
from py_cachify import cached, init_cachify
# Global initialization
init_cachify()
# Local instance with a shorter TTL that wraps the global one
local = init_cachify(is_global=False, prefix='LOCAL-')
@local.cached(key='local-expensive-{x}', ttl=5)
@cached(key='expensive-{x}', ttl=60)
def expensive(x: int) -> int:
return x * 10
In this scenario:
- The outer cache (local instance) provides a short-lived layer over the inner global cache.
- Could be useful to add in-memory cache over a Redis/Dragonfly cache, to further speed up execution (useful for hard to refactor N+1 processing, for example).
- Calling
expensive.reset(x)will: - Clear the local cache entry for that call.
- Attempt to call
reseton the inner cached layer as well, if present, so both layers are cleared for that key.
This makes multi-layer setups behave intuitively when resetting cached values.
Resetting the Cache¶
You can reset the cache for either a synchronous or asynchronous function by calling the reset method attached to the wrapped function.
# Reset cache for a synchronous function
compute_expensive_operation.reset()
# Reset cache for an asynchronous function
await fetch_data.reset(param='param-value')
For instance-based usage, the pattern is the same:
Notes¶
- Ensure that both the serialization and deserialization functions defined in
enc_decare efficient to preserve optimal performance. - If py-cachify is not initialized through
init_cachifywithis_global=True, using the global@cacheddecorator will raise aCachifyInitErrorat runtime. Cachifyinstances created withis_global=Falsedo not depend on global initialization and can be used independently.
Type Hints Remark¶
Currently, Python's type hints have limitations in fully capturing a function's original signature when transitioning to a protocol-based callable in a decorator, particularly for methods (i.e., those that include self). ParamSpec can effectively handle argument and keyword types for functions but doesn't translate well to methods within protocols like WrappedFunctionReset. I'm staying updated on this issue and recommend checking the following resources for more insights into ongoing discussions and proposed solutions:
- Typeshed Pull Request #11662
- Mypy Pull Request #17123
- Python Discussion on Allowing Self-Binding for Generic ParamSpec
Once any developments occur, I will quickly update the source code to incorporate the changes.