From wikipedia:

In computing, memoization is an optimization technique used primarily to speed up computer programs by having function calls avoid repeating the calculation of results for previously processed inputs. Although related to caching, memoization refers to a specific case of this optimization.

We can do the typically recursive example: The Fibonacci secuence. The non-optimized Python code can be:

def fib(n):  
    if n == 1 or n == 0:  
        return n  
    return fib(n-1)+fib(n-2)  
print fib(32)

The optimized code v1 would be:

def fib_opt(n, cache={}):  
    if n in cache:  
        return cache[n]  
    elif (n == 1) or (n==0):  
        return n  
    cache[n] = fib_opt(n-1) + fib_opt(n-2)  
    return cache[n]  
print fib_opt(32)

Let's see the difference just by executing the normal code and the memoized one:

$ time python fib.py  
5702887  
real    0m3.458s  
$ time python fib_opt.py  
5702887  
real    0m0.017s

Same result on both scripts, a little diference on execution time. Why is working? Because we are recording in the dictionary cache the result of the function with a given parameter. This means that we don't need to calculate again fac(5) if we already have done it because the result will be exactly the same. This is called memoization.

How it works? If you use an object as a default parameter in a function argument, Python will create the object just once. What does it means? That the empty dictionary assigned to cache will be initialized just once. If you store a value in this dictionary, this information will be available in the next call to the function. Little crazy, but is not a bug, it's a feature. We can use this feature for implement memoization easily. If you are confused with this explanation, let's see a simple example. The optimazed code above is the same as:

cache = {}  
def fib_opt(n):  
    if n in cache:  
        return cache[n]  
    elif (n == 1) or (n==0):  
        return n  
    cache[n] = fib_opt(n-1) + fib_opt(n-2)  
    return cache[n]  
print fib_opt(32)

More clear right now? As you can see the variable cache is assigned just once, and in the next calls we can use the results previusly stored in this dictionary.

Ok. There are another way of memoize a function (v2): using decorators. We have to create the decorator and use it in the non-optimized function:

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

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

In the decorator definition the cache variable will be initialized just once. As you can see we are using this caracteristic for store all the results of a function. We will check each time the function is called if we already have the computed value or not. If we don't have it, we will compute it and return the value. As you can see the decorators works perfectly well with recursive functions. Again, if we execute the code we can see the improvement.

$ time python fib_dec.py  
5702887  
real    0m0.017s

The main difference between v1 and v2 is that in v2 we are hidding the memoization stuff in a decorator, and the function decorated fib(n) function don't have any cache/memoization logic. This ends up in a more clear code and easy to test: you can test that the decorator is working with a dummy function and you can test alone the function you want to mnemoize. The decorator, of course, is generic and can be used in other functions.

And that's all!! Thanks for reading!

Note: just after writing this article I've found this other article explaining exactly the same but in a more extensive way. Well, they are using Python3 and my version is for being used with python 2.X, but the code of Python3 looks exactly the same.