diff --git a/python-stdlib/functools/README.md b/python-stdlib/functools/README.md new file mode 100644 index 000000000..5c7857531 --- /dev/null +++ b/python-stdlib/functools/README.md @@ -0,0 +1,11 @@ +# functools + +In CPython, `@functools.lru_cache` would also record the *order* of the keyword +arguments as they are passed to the function to cache the output for. So, a +function called with `(a=1, b=2)` and called with `(b=2, a=1)` will take up two +entries in the LRU cache even though they are virtually the same function +invocation. + +MicroPython, however, seems to pre-sort keyword arguments. This means the +keyword arguments order is lost, and therefore the behaviour of +`@functools.lru_cache` differs slightly between the two implementations. diff --git a/python-stdlib/functools/functools.py b/python-stdlib/functools/functools.py index b3c368e8a..ce032d611 100644 --- a/python-stdlib/functools/functools.py +++ b/python-stdlib/functools/functools.py @@ -26,3 +26,35 @@ def reduce(function, iterable, initializer=None): for element in it: value = function(value, element) return value + + +def lru_cache(*args, maxsize=128, **kwargs): + def o(f): + if maxsize == 0: + return f + values = {} + order = [] + + def i(*inner_args, **inner_kwargs): + # dict isn't hashable and kwargs are pre-sorted by key. + key = (inner_args, tuple(inner_kwargs.items())) + if key in values: + output = values[key] + if maxsize: + order.remove(key) + else: + output = f(*inner_args, **inner_kwargs) + if maxsize and len(order) == maxsize: + del values[order.pop()] + values[key] = output + if maxsize: + order.insert(0, key) + return output + + return i + + return o(args[0]) if args else o + + +def cache(*args): + return lru_cache(*args, maxsize=None) diff --git a/python-stdlib/functools/manifest.py b/python-stdlib/functools/manifest.py index 634413c1e..bcb3386a6 100644 --- a/python-stdlib/functools/manifest.py +++ b/python-stdlib/functools/manifest.py @@ -1,3 +1,3 @@ -metadata(version="0.0.7") +metadata(version="0.1.0") module("functools.py") diff --git a/python-stdlib/functools/test_cache.py b/python-stdlib/functools/test_cache.py new file mode 100644 index 000000000..f539d6af1 --- /dev/null +++ b/python-stdlib/functools/test_cache.py @@ -0,0 +1,166 @@ +from functools import lru_cache, cache + + +@lru_cache(maxsize=8) +def f8(i): + global count + count += 1 + return i + + +count = 0 +for i in range(8): + assert f8(i) == i +assert count == 8 +for i in range(8): + assert f8(i) == i +assert count == 8 + +assert f8(100) == 100 +assert count == 9 +assert f8(5) == 5 +assert count == 9 +assert f8(0) == 0 +assert count == 10 + + +@lru_cache +def fdefault(i): + global count + count += 1 + return i + + +count = 0 +for i in range(150): + assert fdefault(i) == i +assert count == 150 +for i in range(149, -1, -1): + assert fdefault(i) == i +assert count == 150 + (150 - 128) + + +@lru_cache(maxsize=0) +def f0(i): + global count + count += 1 + return i + + +count = 0 +for i in range(150): + assert f0(i) == i +assert count == 150 +for i in range(150): + assert f0(i) == i +assert count == 300 + + +@lru_cache(maxsize=None) +def fnone(i): + global count + count += 1 + return i + + +count = 0 +for i in range(150): + assert fnone(i) == i +assert count == 150 +for i in range(150): + assert fnone(i) == i +assert count == 150 + + +@lru_cache +def fretnone(i): + global count + count += 1 + + +count = 0 +for i in range(150): + assert fretnone(i) is None +assert count == 150 +for i in range(149, -1, -1): + assert fretnone(i) is None +assert count == 150 + (150 - 128) + + +@lru_cache +def fargs(i, i2): + global count + count += 1 + return i + + +count = 0 +for i in range(150): + assert fargs(i, i) == i +assert count == 150 +for i in range(149, -1, -1): + assert fargs(i, i) == i +assert count == 150 + (150 - 128) +for i in range(150): + assert fargs(i, 10000) == i +assert count == 150 + (150 - 128) + 150 + + +@lru_cache +def fvarargs(i, *args): + global count + count += 1 + return i + + +count = 0 +for i in range(150): + if i & 1 == 0: + assert fvarargs(i) == i + else: + assert fvarargs(i, 'micropython') == i +assert count == 150 +for i in range(149, -1, -1): + if i & 1 == 0: + assert fvarargs(i) == i + else: + assert fvarargs(i, 'micropython') == i +assert count == 150 + (150 - 128) +for i in range(150): + assert fvarargs(i, 'micropython', 'more micropython') == i +assert count == 150 + (150 - 128) + 150 + + +@lru_cache +def fkwargs(i, i2=None, i3=0, i4="a"): + global count + count += 1 + return i + + +count = 0 +for i in range(150): + assert fkwargs(i, i3=i) == i +assert count == 150 +for i in range(149, -1, -1): + assert fkwargs(i, i3=i) == i +assert count == 150 + (150 - 128) +for i in range(150): + assert fkwargs(i, i3=10000) == i +assert count == 150 + (150 - 128) + 150 + + +@cache +def fcache(i): + global count + count += 1 + return i + + +count = 0 +for i in range(150): + assert fcache(i) == i +assert count == 150 +for i in range(150): + assert fcache(i) == i +assert count == 150 diff --git a/tools/ci.sh b/tools/ci.sh index 2fc6fe90c..fb7d47e9d 100755 --- a/tools/ci.sh +++ b/tools/ci.sh @@ -66,6 +66,7 @@ function ci_package_tests_run { python-stdlib/collections-defaultdict/test_defaultdict.py \ python-stdlib/functools/test_partial.py \ python-stdlib/functools/test_reduce.py \ + python-stdlib/functools/test_cache.py \ python-stdlib/heapq/test_heapq.py \ python-stdlib/hmac/test_hmac.py \ python-stdlib/itertools/test_itertools.py \