<jupyter_start><jupyter_text>1. Imperative and declarative programming training<jupyter_code>import math

def min_imperative(c):
    """by Ilias"""
    minValue = math.inf
    for v in c:
        if v < minValue:
            minValue = v
    return minValue
from functools import reduce

def min_declarative(array):
    """by Ilias"""
    return reduce(lambda a, b: a if (a < b) else b, array)
# don't do that, its just for training. Ilias's desicion is much better

mymin = lambda l, m = 99999999: [(m := elem) for elem in l if elem < m][-1]
tests = [
    [1, 2, 3],
    [3, 2, 1],
    [1],
    [0, -8, 6],
]
for test in tests:
    print(min(test) == min_imperative(test))
    print(min(test) == min_declarative(test))
    # print(min(test) == mymin(test))  # works with Python3.8 +<jupyter_output>True
True
True
True
True
True
True
True<jupyter_text>2. smart_cache decorator that caches N most popular (by arguments usage) results<jupyter_code>@smart_cache(n=2)
def return_video(*args, **kwargs):
    return '&' * 1024 * 1024 * 1024
return_video(a=1)  # calculation
return_video(a=2)  # calculation
return_video(a=1)  # using cached
return_video(a=3)  # calculation
return_video(a=1)  # using cached
return_video(a=3)  # using cached or calculation
return_video(a=2)  # using cached or calculation
return_video(a=2)  # using cached<jupyter_output><empty_output><jupyter_text>**step 1** - creating a decorator with arguments<jupyter_code>import functools


def smart_cache(cache_size=10):

    def wrapper_helper(func):

        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            print(f'cache_size={cache_size}')
            return func(*args, **kwargs)

        return wrapper

    return wrapper_helper
@smart_cache(cache_size=2)
def foo(bar):
    pass

foo(1)<jupyter_output>cache_size=2<jupyter_text>**step 2** - creating just a cache decorator<jupyter_code>import functools


def smart_cache(cache_size=10):
    cached_arguments = []  # [(args, kwargs), ...]
    cached_results = {}    # {<index of (args, kwargs)>: <cached result>}

    def wrapper_helper(func):

        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            try:
                index = cached_arguments.index((args, kwargs))
            except ValueError:
                index = None

            if index is not None:
                return cached_results[index]

            res = func(*args, **kwargs)

            index = len(cached_arguments)
            cached_arguments.append((args, kwargs))
            cached_results[index] = res

            print(cached_arguments)
            print(cached_results)

            return res

        return wrapper

    return wrapper_helper
@smart_cache(cache_size=2)
def foo(bar):
    print(f'bar is {bar}')

foo(1)
foo(1)
foo(2)
foo(2)
foo(3)
foo(3)<jupyter_output>bar is 1
[((1,), {})]
{0: None}
bar is 2
[((1,), {}), ((2,), {})]
{0: None, 1: None}
bar is 3
[((1,), {}), ((2,), {}), ((3,), {})]
{0: None, 1: None, 2: None}<jupyter_text>**step 3** - counting statistics of arguments usage<jupyter_code>import functools


def smart_cache(cache_size=10):
    cached_arguments = []        # [(args, kwargs), ...]
    cached_arguments_usage = {}  # {<index of (args, kwargs)>: <count of its usage>}
    cached_results = {}          # {<index of (args, kwargs)>: <cached result>}

    def wrapper_helper(func):

        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            try:
                index = cached_arguments.index((args, kwargs))
            except ValueError:
                index = None
            
            if index is not None:
                cached_arguments_usage[index] += 1
                return cached_results[index]

            res = func(*args, **kwargs)

            index = len(cached_arguments)
            cached_arguments.append((args, kwargs))
            cached_arguments_usage[index] = 1
            cached_results[index] = res

            print(cached_arguments)
            print(cached_arguments_usage)
            print(cached_results)

            return res

        return wrapper

    return wrapper_helper
@smart_cache(cache_size=2)
def foo(bar):
    print(f'bar is {bar}')

foo(1)
foo(1)
foo(2)
foo(2)
foo(3)
foo(3)<jupyter_output>bar is 1
[((1,), {})]
{0: 1}
{0: None}
bar is 2
[((1,), {}), ((2,), {})]
{0: 2, 1: 1}
{0: None, 1: None}
bar is 3
[((1,), {}), ((2,), {}), ((3,), {})]
{0: 2, 1: 2, 2: 1}
{0: None, 1: None, 2: None}<jupyter_text>**step 4** - use usage stats to remove not popluar cached results<jupyter_code>import functools


def smart_cache(cache_size=10):
    cached_arguments = []        # [(args, kwargs), ...]
    cached_arguments_usage = {}  # {<index of (args, kwargs)>: <count of its usage>}
    cached_results = {}          # {<index of (args, kwargs)>: <cached result>}

    def wrapper_helper(func):

        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            try:
                index = cached_arguments.index((args, kwargs))
            except ValueError:
                index = None
            
            if index is not None:
                cached_arguments_usage[index] += 1
                if index in cached_results:
                    return cached_results[index]

            res = func(*args, **kwargs)

            if index is None:
                index = len(cached_arguments)
                cached_arguments.append((args, kwargs))
                cached_arguments_usage[index] = 1

            cached_results[index] = res

            if len(cached_results) > cache_size:
                cached_results_indexes = list(cached_results.keys())

                index_with_smallest_usage = cached_results_indexes[0]
                smallest_usage = cached_arguments_usage[index_with_smallest_usage]
                for i in cached_results_indexes:
                    if cached_arguments_usage[i] < smallest_usage:
                        index_with_smallest_usage = i
                        smallest_usage = cached_arguments_usage[i]
                del cached_results[index_with_smallest_usage]

            print(cached_arguments)
            print(cached_arguments_usage)
            print(list(cached_results.keys()))
            print()

            return res

        return wrapper

    return wrapper_helper
@smart_cache(cache_size=2)
def return_video(*args, **kwargs):
    return '&' * 1024 * 1024 * 1024
result = return_video(1, 2, 3, a=4, b=5, c=6)
result = return_video(1, 2, 3, a=4, b=5, c=7)
result = return_video(1, 2, 3, a=4, b=5, c=8)
result = return_video(1, 2, 3, a=4, b=5, c=6)
result = return_video(1, 2, 3, a=4, b=5, c=8)
result = return_video(1, 2, 3, a=4, b=5, c={1: 2, 3: {1, 2, 3}})
result = return_video(1, 2, 3, a=4, b=5, c={1: 2, 3: {1, 2, 3}})
result = return_video(1, 2, 3, a=4, b=5, c={1: 2, 3: {1, 2, 3}})
len(result)<jupyter_output><empty_output><jupyter_text>Dict is unhashable even inside tuple<jupyter_code>{('a', {})}
{('a', ('b', ('c', {})))}  # even when it is very very deep<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.1367315064603152, 'fraction_numerical': 0.027829050120687208, 'mean_word_length': 2.6497409326424872, 'pattern_counts': {'":': 0, '<': 39, '<?xml version=': 0, '>': 36, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 1, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '7', 'dir': 'jupyter-structured-clean-dedup', 'id': '264309', 'license': 'MIT', 'n_tokens_mistral': 2485, 'n_tokens_neox': 2350, 'n_words': 589, 'path': 'week04_functions2/seminar/seminar04.ipynb', 'repo_name': 'fortminors/msai-python', 'size': '7043'}