Why does adding the decorator @lru_cache(from functools) break this function?

Question:

The function is a part of the solution to the following problem:

"Find all valid combinations of k numbers that sum up to n such that the following conditions are true:

Only numbers 1 through 9 are used.
Each number is used at most once.

Return a list of all possible valid combinations. The list must not contain the same combination twice, and the combinations may be returned in any order."

An example test case which fails with the code:

import functools

@functools.lru_cache(maxsize=None)
def CombSum(k,n,num):
    if k<=0 or n<=0 or num>9:
        return []
    if k==1 and n>=num and n<=9:
        return [[n]]
    ans=[]
    while num <= 9:
        for arr in CombSum(k-1,n-num,num+1):
            arr.append(num)
            ans.append(arr)
        num+=1
    return ans
print(CombSum(4,16,1))

produces

[[9, 4, 2, 1], [8, 5, 2, 1], [7, 6, 2, 1], [8, 4, 3, 1], [7, 5, 3, 1], [6, 5, 4, 1, 5, 3, 2], [7, 4, 3, 2], [6, 5, 4, 1, 5, 3, 2]]

The above result is incorrect and when I remove the decorator below, everything works fine:

import functools

def CombSum(k,n,num):
    if k<=0 or n<=0 or num>9:
        return []
    if k==1 and n>=num and n<=9:
        return [[n]]
    ans=[]
    while num <= 9:
        for arr in CombSum(k-1,n-num,num+1):
            arr.append(num)
            ans.append(arr)
        num+=1
    return ans
print(CombSum(4,16,1))

produces

[[9, 4, 2, 1], [8, 5, 2, 1], [7, 6, 2, 1], [8, 4, 3, 1], [7, 5, 3, 1], [6, 5, 4, 1], [7, 4, 3, 2], [6, 5, 3, 2]]

This is the correct answer.

Why is the decorator breaking the function?

Asked By: A-ar

||

Answers:

You violated the decorator’s contract.
You’re mutating elements of the returned list.
The decorator should only be applied to Pure functions.

You defined a Public API that returns a list of lists.
Switch to returning an immutable tuple of tuples.
Then your approach will be compatible with the LRU decorator.

Answered By: J_H

Use a immutable object such as a tuple and it will work

@functools.lru_cache(maxsize=None)
def CombSum2(k,n,num):
    if k<=0 or n<=0 or num>9:
        return tuple()
    if k==1 and n>=num and n<=9:
        return (n,),
    ans = tuple()
    while num <= 9:
        for arr in CombSum2(k-1,n-num,num+1):
            arr += num,
            ans += arr,
        num+=1
    return ans
Answered By: cards

Your code also works if you do not modify the list returned by CombSum(k-1,n-num,num+1) and make a copy with the new element added instead. Otherwise you are changing the cached result, which makes the return value inconsistent for the same input.

@functools.cache
def CombSum(k,n,num):
    if k<=0 or n<=0 or num>9:
        return []
    if k==1 and n>=num and n<=9:
        return [[n]]
    ans=[]
    while num <= 9:
        for arr in CombSum(k-1,n-num,num+1):
            ans.append(arr + [num])
        num+=1
    return ans
print(CombSum(4,16,1))
Answered By: Unmitigated