pythonfunction-attributes

Reassign a function attribute makes it 'unreachable'


I have a simple little decorator, that caches results from function calls in a dict as a function attribute.

from decorator import decorator
def _dynamic_programming(f, *args, **kwargs):
    try:
        f.cache[args]
    except KeyError:
        f.cache[args] = f(*args, **kwargs)
    return f.cache[args]

def dynamic_programming(f):
    f.cache = {}
    return decorator(_dynamic_programming, f)

I now want to add the possibility to empty the cache. So I change the dynamic_programming() function like this:

def dynamic_programming(f):
    f.cache = {}
    def clear():
        f.cache = {}
    f.clear = clear
    return decorator(_dynamic_programming, f)

Now let's assume I use this little thing to implement a Fibonacci number function:

@dynamic_programming
def fib(n):
    if n <= 1:
        return 1
    else:
        return fib(n-1) + fib(n-2)

>>> fib(4)
5
>>> fib.cache
{(0,): 1, (1,): 1, (2,): 2, (3,): 3, (4,): 5}

But now when I clear the cache something weird happens:

>>> fib.clear()
>>> fib.cache
{(0,): 1, (1,): 1, (2,): 2, (3,): 3, (4,): 5}

Or (with a new Python kernel running) do it the other way round:

>>> fib.clear()
>>> fib(4)
5
>>> fib.cache
{}

Why is the cache somehow not 'reachable' after the first access to it, i.e. not changing when calling clear() after a call or a call after a clear()?

(Btw. I know a solution to clear the cache correctly: calling f.cache.clear() instead of assigning {} to it works as expected. I'm merely interested in the reason why the assigning solution fails.)


Solution

  • The problem is with the decorator module. If you add some print statements to your decorator:

    from decorator import decorator
    def _dynamic_programming(f, *args, **kwargs):
        print "Inside decorator", id(f.cache)
        try:
            f.cache[args]
        except KeyError:
            f.cache[args] = f(*args, **kwargs)
        return f.cache[args]
    
    def dynamic_programming(f):
        f.cache = {}
        print "Original cache", id(f.cache)
        def clear():
            f.cache = {}
            print "New cache", id(f.cache)
        f.clear = clear
        return decorator(_dynamic_programming, f)
    
    @dynamic_programming
    def fib(n):
        if n <= 1:
            return 1
        else:
            return fib(n-1) + fib(n-2)
    
    print fib(4)
    print id(fib.cache)
    fib.clear()
    print id(fib.cache)
    print fib(10)
    print id(fib.cache)
    

    It outputs (duplicate lines skipped):

    Original cache 139877501744024
    Inside decorator 139877501744024
    5
    139877501744024
    New cache 139877501802208
    139877501744024
    Inside decorator 139877501802208
    89
    139877501744024
    

    As you can see, the cache inside the decorator changes according the the clear function. However, the cache accessed from __main__ does not change. Printing the cache outside and inside the decorator give a clearer picture (again, duplicates skipped):

    Inside decorator {}
    Inside decorator {(1,): 1}
    Inside decorator {(2,): 2, (0,): 1, (1,): 1}
    Inside decorator {(2,): 2, (0,): 1, (3,): 3, (1,): 1}
    5
    Outside {(2,): 2, (0,): 1, (3,): 3, (1,): 1, (4,): 5}
    Inside decorator {}
    Inside decorator {(1,): 1}
    Inside decorator {(2,): 2, (0,): 1, (1,): 1}
    Inside decorator {(2,): 2, (0,): 1, (3,): 3, (1,): 1}
    Inside decorator {(2,): 2, (0,): 1, (3,): 3, (1,): 1, (4,): 5}
    Inside decorator {(0,): 1, (1,): 1, (2,): 2, (3,): 3, (4,): 5, (5,): 8}
    Inside decorator {(0,): 1, (1,): 1, (2,): 2, (3,): 3, (4,): 5, (5,): 8, (6,): 13}
    Inside decorator {(0,): 1, (1,): 1, (2,): 2, (3,): 3, (4,): 5, (5,): 8, (6,): 13, (7,): 21}
    Inside decorator {(0,): 1, (1,): 1, (2,): 2, (8,): 34, (3,): 3, (4,): 5, (5,): 8, (6,): 13, (7,): 21}
    Inside decorator {(0,): 1, (1,): 1, (2,): 2, (8,): 34, (3,): 3, (9,): 55, (4,): 5, (5,): 8, (6,): 13, (7,): 21}
    89
    Outside {(2,): 2, (0,): 1, (3,): 3, (1,): 1, (4,): 5}
    

    As you can see, the inside changes are not echoed on the outside. The problem is that inside the decorator module, there is the line (inside the class it used to make the decorator):

    self.dict = func.__dict__.copy()
    

    And then later:

    func.__dict__ = getattr(self, 'dict', {})
    

    So basically, the __dict__ on the outside is different from the __dict__ on the inside. This means that:

    So, to summarise, it's a problem with the decorator module.