From 753843f1f8da371333aeb648116bcf0369f588f3 Mon Sep 17 00:00:00 2001 From: Alessandro Gatti Date: Mon, 7 Sep 2026 06:20:25 +0200 Subject: [PATCH] functools: Add an implementation for lru_cache. This commit adds an implementation for both `lru_cache` and `cache` to the `functools` module. If an unbounded cache is requested, then the function doesn't keep track of which entry was used last (so this becomes a generic `dict` wrapper). Since dicts aren't hashable in MicroPython, keyword arguments aren't serialised internally as a dictionary to build part of the cached lookup key, but as a tuple of (key, value) tuples. This takes up a bit more space in memory than a flattened tuple of interleaved keys and values, but that would take up more space when compiled. The CI script was also modified to include the new cache-related tests (part of this commit) to the test suite. These changes increase the size of the functools module by 289 bytes once compiled. Signed-off-by: Alessandro Gatti --- python-stdlib/functools/README.md | 11 ++ python-stdlib/functools/functools.py | 32 +++++ python-stdlib/functools/manifest.py | 2 +- python-stdlib/functools/test_cache.py | 166 ++++++++++++++++++++++++++ tools/ci.sh | 1 + 5 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 python-stdlib/functools/README.md create mode 100644 python-stdlib/functools/test_cache.py 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 \