Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Understanding parameter handling in a python memoization decorator

I have been using this excellent decorator for memoization, which I found on the web (shown here with the Fibonacci sequence as an example):

def memoize(f):
    cache= {}
    def memf(*x):
        if x not in cache:
            cache[x] = f(*x)
        return cache[x]
    return memf

@memoize
def fib(n):
    if n==1 or n==0:
        return 1
    return fib(n-2) + fib(n-1)

print fib(969)

Now I'd like to understand the inner workings a bit better, I have not found answers by reading up on decorators or parameter handling in Python.

Why is the cache dictionary not reinitialized every time the decorated function is called?

How is *x recognized to be the parameters sent to the decorated function, i.e., 969 in the function call fib(969)?

like image 645
Anders S Avatar asked Apr 04 '12 12:04

Anders S


1 Answers

The decorator is called only once, immediately after the decorated function is first defined. Thus, these two techniques(using @wrap and bar = wrap(bar)) are the same:

>>> def wrap(f):
...     print 'making arr'
...     arr = []
...     def inner():
...         arr.append(2)
...         print arr
...         f()
...     return inner
...     
>>> @wrap
... def foo():
...     print 'foo was called'
...     
making arr
>>> foo()
[2]
foo was called
>>> foo()
[2, 2]
foo was called
>>> def bar():
...     print 'bar was called'
...     
>>> bar = wrap(bar)
making arr
>>> bar()
[2]
bar was called

In both cases it is clear that arr is created only when wrap(f) is called, and wrap is called only when foo and bar are first declared.

As for the case of passing arguments to a decorated function, remember that a decorator takes a function as a parameter and returns a modified version of that function. So a decorator typically takes one parameter, which is the function that it is modifying. It returns a new function, and the decorator can define the function that it returns as taking any number of arguments(for example, *args). The decorator can even return a function that takes too many parameters for the method that it decorates.

>>> def wrap_with_arg(f):
...     def wrap(*args):
...         print 'called with %d arguments' % len(args)
...         f(args)
...     return wrap
...     
>>> @wrap_with_arg
... def baz(arg):
...     print 'called with argument %r' % arg
...     
>>> baz(3)
called with 1 arguments
called with argument 3
>>> baz(3, 4)
called with 2 arguments
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "<input>", line 4, in wrap
  File "<input>", line 3, in baz
TypeError: not all arguments converted during string formatting

While eventually baz throws an error, notice how the number of arguments is correctly printed before the error is thrown.

like image 133
Nolen Royalty Avatar answered Sep 29 '22 12:09

Nolen Royalty