<jupyter_start><jupyter_text>About This NotebookThis notebook contains examples of three different ways that the CacheManager can be used. What follows is an example of using the `yaci.dict_storage.DictStorage` storage implementation with the `yaci.CacheManager` as a:* A decorator* A context manager* An argument to a class (This is the most flexible and gives the user the most control over caching)<jupyter_code>from yaci import cache_manager
from yaci import contexts
from yaci import dict_storage
storage = dict_storage.DictStorage()
cm = cache_manager.CacheManager(storage=storage)<jupyter_output><empty_output><jupyter_text>Decorating FunctionsThe `decorator` implementaiton of the cache manager is niave, but the code in `yaci.contexts` can be used as a reference implementation for more advanced usages should they be needed. This pattern may be required if you are working with a third-party function that you cannot modify.<jupyter_code>@contexts.cache()
def double(x):
    print("Executing 'double' with arguments {}".format(x))
    return x * 2<jupyter_output><empty_output><jupyter_text>Even though we call `double` four times, when the function is invoked with an argument that it has already seen we do not actually invoke the function, but use the value stored in the cache. This is made clear by the fact that we only see the line starting with "Executing 'double'..." two times.<jupyter_code>args = [3, 3, 4, 4]
[double(x) for x in args]<jupyter_output>Executing 'double' with arguments 3
Executing 'double' with arguments 4<jupyter_text>Context ManagerI have to be honest this is one of those examples that I thought might be useful, but couldn't really figure out how I might use it in the real world. This example exposes the magic of the `yaci.CacheManger` class, which is the `default_get` funciton. Here you need to be aware of how you define the key. Different stroage implementations will have different rules about what values can be used as "keys" so if you decide to use a storage implementation other than the `DictStorage` implementation you'll need to make sure that the value that use as a key is acceptable to the storage implementation.<jupyter_code>def tripple(x):
    print("Executing 'tripple' with arguments {}".format(x))
    return x**3
results = list()
with contexts.CacheContext() as cache:
    results = [cache.default_get((tripple, x), tripple, x) for x in args]
print(results)<jupyter_output>Executing 'tripple' with arguments 3
Executing 'tripple' with arguments 4
[27, 27, 64, 64]<jupyter_text>Argument to Class or FunctionBelow is an example of an instance where you want to implement caching via composition, by passing a CacheManger as an argument to a class or function. This pattern provides the most flexibility form an implementation standpoint. Using the CacheManager with Functions<jupyter_code>def run_expensive_function(function, arg, cache=None ):
    cache = cache if cache is not None else cache_manager.CacheManager(storage=dict_storage.NoopDictStorage())
    result = cache.default_get((function, arg), function, arg)
    return result

def some_expensive_function(x):
    print("Running Expensive Function with argument {}".format(x))
    return x<jupyter_output><empty_output><jupyter_text>In the code below we do not speify a cache manager, and we can see that `some_expensive_function` is invoked on each iteration.<jupyter_code>queue = [1, 2, 3,1, 3, 2]
f_results = list()
for i in range(len(queue)):
    f_result = run_expensive_function(some_expensive_function, queue.pop())
    f_results.append(f_result)
print(f_results)<jupyter_output>Running Expensive Function with argument 2
Running Expensive Function with argument 3
Running Expensive Function with argument 1
Running Expensive Function with argument 3
Running Expensive Function with argument 2
Running Expensive Function with argument 1
[2, 3, 1, 3, 2, 1]<jupyter_text>In the code below we **do** specify a cache manger and we note that the `some_expensive_function` is only invoked when it encounters new arguments.<jupyter_code>func_cache = cache_manager.CacheManager(storage=dict_storage.DictStorage())
queue2 = [1, 2, 3,1, 3, 2]
f_results2 = list()
for i in range(len(queue2)):
    f_result = run_expensive_function(some_expensive_function, queue2.pop(), cache=func_cache)
    f_results2.append(f_result)
print(f_results2)<jupyter_output>Running Expensive Function with argument 2
Running Expensive Function with argument 3
Running Expensive Function with argument 1
[2, 3, 1, 3, 2, 1]<jupyter_text>Using the CacheManager with a Class<jupyter_code>import cool_api

class SomeApiClient(object):
    def __init__(self, name, cache=None):
        self.name = name
        self.cache = cache if cache is not None else cache_manager.CacheManager(dict_storage.NoopDictStorage())
    
    def get(self, *args):
        result = self.cache.default_get((cool_api.get, args), cool_api.get, *args)
        return result
cm = cache_manager.CacheManager(dict_storage.DictStorage())

client = SomeApiClient('c1', cache=cm)

print(client.get('foo'))
print(client.get('foo'))<jupyter_output>In cool_api with args ('foo',)
('foo',)
('foo',)
{'fraction_non_alphanumeric': 0.0785188057386584, 'fraction_numerical': 0.011244668476153548, 'mean_word_length': 5.614102564102564, 'pattern_counts': {'":': 0, '<': 28, '<?xml version=': 0, '>': 28, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '8', 'dir': 'jupyter-structured-clean-dedup', 'id': '187668', 'license': 'MIT', 'n_tokens_mistral': 1520, 'n_tokens_neox': 1452, 'n_words': 604, 'path': 'examples/.ipynb_checkpoints/Cache_Manager_Examples-checkpoint.ipynb', 'repo_name': 'aabdullah-bos/yaci', 'size': '5158'}