Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.)

like image 809
hildensia Avatar asked Oct 29 '14 16:10

hildensia


2 Answers

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:

  • The __dict__ is copied (not referenced) by the decorator
  • When the cache changes, it changes the inside __dict__, not the outside __dict__
  • Therefore, the cache used by the _dynamic_programming is cleared, but you cannot see that from the outside, as the decorator's __dict__ is still pointing to the old cache (as you can see above, as the inside cache updates, while the outside cache remains the same)

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

like image 153
matsjoyce Avatar answered Nov 05 '22 14:11

matsjoyce


so @matsjoyce's answer is very interesting and in-depth and i know you already have the solution, but i always find it a little clearer to write my own decorators:

def dynamic_programming(f):
    def wrapper(*args, **kwargs):
        try:
            return wrapper.cache[args]            
        except KeyError:
            res = wrapper.cache[args] = f(*args, **kwargs)
            return res
    wrapper.cache = {}
    wrapper.clear = wrapper.cache.clear
    return wrapper
like image 26
acushner Avatar answered Nov 05 '22 14:11

acushner