406

I just started Python and I've got no idea what memoization is and how to use it. Also, may I have a simplified example?

Michael Durrant
  • 84,444
  • 83
  • 284
  • 429
blur959
  • 4,381
  • 4
  • 16
  • 6
  • 229
    When the second sentence of the relevant wikipedia article contains the phrase "mutually-recursive descent parsing[1] in a general top-down parsing algorithm[2][3] that accommodates ambiguity and left recursion in polynomial time and space," I think it is entirely appropriate to ask SO what is going on. – Clueless Jan 02 '10 at 14:17
  • 12
    @Clueless: That phrase is preceded by "Memoization has also been used in other contexts (and for purposes other than speed gains), such as in". So it's just a list of examples (and need not be understood); it's not part of the explanation of memoization. – ShreevatsaR Apr 04 '14 at 06:12
  • Here is a good explanation with attached examples of memoization and how to incorporate it into a decorator: http://www.pycogsci.info/?p=221 – Stefan Gruenwald May 24 '14 at 19:08
  • 1
    @StefanGruenwald That link is dead. Can you please find an update? – JS. Dec 03 '14 at 17:39
  • 2
    New link to pdf file, since pycogsci.info is down: http://people.ucsc.edu/~abrsvn/NLTK_parsing_demos.pdf – Stefan Gruenwald Dec 05 '14 at 20:08
  • You can look at my blog post at u8y7541.github.io/blog_posts/lambdas_recursion_memoizing.html –  Nov 30 '15 at 02:51
  • 7
    @Clueless, The article actually says "**simple** mutually-recursive descent parsing[1] in a general top-down parsing algorithm[2][3] that accommodates ambiguity and left recursion in polynomial time and space". You missed the **simple**, which obviously makes that example much clearer :). – studgeek Jul 30 '17 at 00:48
  • seeing how many people answered and are still answering this question makes be a believer in the "BIKE SHED EFFECT" https://en.wikipedia.org/wiki/Law_of_triviality – A_P Jan 05 '19 at 19:41

13 Answers13

379

Memoization effectively refers to remembering ("memoization" → "memorandum" → to be remembered) results of method calls based on the method inputs and then returning the remembered result rather than computing the result again. You can think of it as a cache for method results. For further details, see page 387 for the definition in Introduction To Algorithms (3e), Cormen et al.

A simple example for computing factorials using memoization in Python would be something like this:

factorial_memo = {}
def factorial(k):
    if k < 2: return 1
    if k not in factorial_memo:
        factorial_memo[k] = k * factorial(k-1)
    return factorial_memo[k]

You can get more complicated and encapsulate the memoization process into a class:

class Memoize:
    def __init__(self, f):
        self.f = f
        self.memo = {}
    def __call__(self, *args):
        if not args in self.memo:
            self.memo[args] = self.f(*args)
        #Warning: You may wish to do a deepcopy here if returning objects
        return self.memo[args]

Then:

def factorial(k):
    if k < 2: return 1
    return k * factorial(k - 1)

factorial = Memoize(factorial)

A feature known as "decorators" was added in Python 2.4 which allow you to now simply write the following to accomplish the same thing:

@Memoize
def factorial(k):
    if k < 2: return 1
    return k * factorial(k - 1)

The Python Decorator Library has a similar decorator called memoized that is slightly more robust than the Memoize class shown here.

Richard
  • 44,865
  • 24
  • 144
  • 216
jason
  • 220,745
  • 31
  • 400
  • 507
  • I test your two examples in IPython's %timeit. When I use the dictionary (the first example) most of the time I get the first calls executed faster than the memorized second calls. Could you check in your system too? There is a great speed-ups (up to 40X) when I tested the second example. Also could you tell me how to access self.memo name later when the execution is finished? – Gökhan Sever Mar 11 '10 at 22:38
  • OK, please ignore the first part of my question because IPython's timeit makes multiple calls while timing the execution of a function. However the self.memo part is still valid :) – Gökhan Sever Mar 11 '10 at 23:13
  • 2
    Thanks for this suggestion. The Memoize class is an elegant solution which can easily be applied to existing code without needing much refactoring. – Captain Lepton Apr 11 '13 at 12:41
  • 10
    The Memoize class solution is buggy, it will not work the same as the `factorial_memo`, because the `factorial` inside `def factorial` still calls the old unmemoize `factorial`. – adamsmith Aug 06 '13 at 07:35
  • 2
    I think @dlutxx has a point. The first version can compute, say, the 50th Fibonacci number quickly *the first time*, whereas the second version with Memoize can't. – JohnJamesSmith0 Dec 21 '13 at 21:40
  • 9
    By the way, you can also write `if k not in factorial_memo:`, which reads better than `if not k in factorial_memo:`. – ShreevatsaR Apr 04 '14 at 06:34
  • 1
    @adamsmith: I checked that the factorial inside def factorial uses the memoized version. – zk82 Jul 13 '14 at 20:43
  • 5
    Should really do this as a decorator. – Emlyn O'Regan Oct 08 '14 at 04:23
  • 2
    Be careful to make a deep copy if you're going to mutate the output otherwise you will inadvertently mutate your cache. – Matthew Molloy Jan 12 '15 at 09:15
  • can `memoize` in python handle recursive functions? – Jackson Tale Jan 23 '15 at 16:54
  • 1
    @JacksonTale: The example given shows memoizing a recursive function! – jason Mar 29 '15 at 02:08
  • I don't see why the factorial function would be profitable in the first call. It may be profitable in the subsequent calls. – TheRandomGuy May 27 '16 at 08:00
  • Theres a rule that really ought be stated about Memoization. Any function can be memoized, on the condition that 1) Its deterministic, and 2) There are no side effects coming in or out of the function. In other words , if the function is not setting , or relying on values outside what it returns and what goes in the brackets, you can memoize. If this isn't true, expect trouble. Avoiding side effects is generally good programming (And mandatory in true functional languages like haskel) and will save you a hell of a lot of heartache debugging. But its not always OO compatible, so choose wisely. – Shayne Sep 05 '16 at 12:02
  • But in the third block you are not passing value for `k`? – vimal1083 Oct 09 '16 at 11:01
  • Does this even work? `args` is a list and you can't put a list in a dictionary key, right? – durden2.0 Nov 03 '16 at 10:37
  • 3
    @durden2.0 I know this is an old comment, but `args` is a tuple. `def some_function(*args)` makes args a tuple. – Adam Smith Dec 13 '16 at 18:18
  • Just curious, in this case, when all the processes are done, how would one clear the cached data? – Y.Du Mar 30 '21 at 03:32
249

New to Python 3.2 is functools.lru_cache. By default, it only caches the 128 most recently used calls, but you can set the maxsize to None to indicate that the cache should never expire:

import functools

@functools.lru_cache(maxsize=None)
def fib(num):
    if num < 2:
        return num
    else:
        return fib(num-1) + fib(num-2)

This function by itself is very slow, try fib(36) and you will have to wait about ten seconds.

Adding lru_cache annotation ensures that if the function has been called recently for a particular value, it will not recompute that value, but use a cached previous result. In this case, it leads to a tremendous speed improvement, while the code is not cluttered with the details of caching.


Python 3.9 released a new function functools.cache which is equivalent to lru_cache(maxsize=None) but with a shorter name:

@functools.cache
def fib(num):
    # etc
Flimm
  • 97,949
  • 30
  • 201
  • 217
  • 2
    Tried fib(1000), got RecursionError: maximum recursion depth exceeded in comparison – Andreas K. Sep 28 '17 at 12:04
  • 5
    @Andyk Default Py3 recursion limit is 1000. The first time `fib` is called, it will need to recur down to the base case before memoization can happen. So, your behavior is just about expected. – Quelklef Aug 19 '18 at 02:07
  • 1
    If I'm not mistaken, it caches only until the process is not killed, right? Or does it cache regardless of whether the process is killed? Like, say I restart my system - will the cached results still be cached? – Kristada673 Oct 22 '18 at 02:20
  • 1
    @Kristada673 Yes, it's stored in the process' memory, not on disk. – Flimm Oct 22 '18 at 07:19
  • Does this introduce significantly more latency than implementing an ad-hoc memoization routine? – Pranav Vempati Oct 25 '18 at 19:24
  • @PranavVempati No, why would it? – Flimm Oct 25 '18 at 19:32
  • 2
    Note that this speeds up even the first run of the function, since it's a recursive function and is caching its own intermediate results. Might be good to illustrate a non-recursive function that's just inherently slow to make it clearer to dummies like me. :D – endolith Aug 02 '19 at 14:41
  • Neat: `fib.cache_info()` might print something cool like `CacheInfo(hits=400, misses=401, maxsize=None, currsize=401)` – Ahmed Fasih Aug 29 '19 at 03:00
  • This worked VERY well for my use case where I had to keep grabbing a large data set over and over again (using `read_csv` and `read_sql` to grab two dataframes). Caching it using this method resulted in a speed improvement of about 27 times. Thanks! – horcle_buzz Dec 09 '19 at 20:29
  • 1
    New in 3.9 is [`functools.cache`](https://docs.python.org/3/library/functools.html#functools.cache) which is (in [cpython](https://github.com/python/cpython/blob/master/Lib/functools.py#L653) at least) a wrapper for `lru_cache(maxsize=None)` but with a shorter name. – Amndeep7 Mar 17 '21 at 06:14
61

The other answers cover what it is quite well. I'm not repeating that. Just some points that might be useful to you.

Usually, memoisation is an operation you can apply on any function that computes something (expensive) and returns a value. Because of this, it's often implemented as a decorator. The implementation is straightforward and it would be something like this

memoised_function = memoise(actual_function)

or expressed as a decorator

@memoise
def actual_function(arg1, arg2):
   #body
Noufal Ibrahim
  • 66,768
  • 11
  • 123
  • 160
19

Memoization is keeping the results of expensive calculations and returning the cached result rather than continuously recalculating it.

Here's an example:

def doSomeExpensiveCalculation(self, input):
    if input not in self.cache:
        <do expensive calculation>
        self.cache[input] = result
    return self.cache[input]

A more complete description can be found in the wikipedia entry on memoization.

Bryan Oakley
  • 310,202
  • 36
  • 445
  • 584
  • Hmm, now if that was correct Python, it would rock, but it appears not to be... okay, so "cache" is not a dict? Because if it is, it should be `if input not in self.cache` and `self.cache[input]` (`has_key` is obsolete since... early in the 2.x series, if not 2.0. `self.cache(index)` was never correct. IIRC) – Jürgen A. Erhard Jan 01 '10 at 15:46
16

Let's not forget the built-in hasattr function, for those who want to hand-craft. That way you can keep the mem cache inside the function definition (as opposed to a global).

def fact(n):
    if not hasattr(fact, 'mem'):
        fact.mem = {1: 1}
    if not n in fact.mem:
        fact.mem[n] = n * fact(n - 1)
    return fact.mem[n]
David
  • 518
  • 4
  • 14
Karel Kubat
  • 1,457
  • 10
  • 11
  • 2
    This seems like a very expensive idea. For every n, it not only caches the results for n, but also for 2 ... n-1. – codeforester Jun 27 '19 at 08:45
15

I've found this extremely useful

from functools import wraps


def memoize(function):    
    memo = {}
        
    @wraps(function)
    def wrapper(*args):

        # add the new key to dict if it doesn't exist already  
        if args not in memo:
            memo[args] = function(*args)

        return memo[args]

    return wrapper
    
    
@memoize
def fibonacci(n):
    if n < 2: return n
    return fibonacci(n - 1) + fibonacci(n - 2)
    
fibonacci(25)
mr.bjerre
  • 1,232
  • 10
  • 26
6

Memoization is basically saving the results of past operations done with recursive algorithms in order to reduce the need to traverse the recursion tree if the same calculation is required at a later stage.

see http://scriptbucket.wordpress.com/2012/12/11/introduction-to-memoization/

Fibonacci Memoization example in Python:

fibcache = {}
def fib(num):
    if num in fibcache:
        return fibcache[num]
    else:
        fibcache[num] = num if num < 2 else fib(num-1) + fib(num-2)
        return fibcache[num]
Aaron Hall
  • 291,450
  • 75
  • 369
  • 312
Romaine Carter
  • 607
  • 11
  • 23
  • 2
    For more performance pre-seed your fibcache with the first few known values, then you can take the extra logic for handling them out of the 'hot path' of the code. – jkflying May 21 '14 at 05:59
5

Well I should answer the first part first: what's memoization?

It's just a method to trade memory for time. Think of Multiplication Table.

Using mutable object as default value in Python is usually considered bad. But if use it wisely, it can actually be useful to implement a memoization.

Here's an example adapted from http://docs.python.org/2/faq/design.html#why-are-default-values-shared-between-objects

Using a mutable dict in the function definition, the intermediate computed results can be cached (e.g. when calculating factorial(10) after calculate factorial(9), we can reuse all the intermediate results)

def factorial(n, _cache={1:1}):    
    try:            
        return _cache[n]           
    except IndexError:
        _cache[n] = factorial(n-1)*n
        return _cache[n]
yegle
  • 5,565
  • 6
  • 35
  • 59
5

Memoization is the conversion of functions into data structures. Usually one wants the conversion to occur incrementally and lazily (on demand of a given domain element--or "key"). In lazy functional languages, this lazy conversion can happen automatically, and thus memoization can be implemented without (explicit) side-effects.

Conal
  • 18,206
  • 2
  • 34
  • 40
4

Here is a solution that will work with list or dict type arguments without whining:

def memoize(fn):
    """returns a memoized version of any function that can be called
    with the same list of arguments.
    Usage: foo = memoize(foo)"""

    def handle_item(x):
        if isinstance(x, dict):
            return make_tuple(sorted(x.items()))
        elif hasattr(x, '__iter__'):
            return make_tuple(x)
        else:
            return x

    def make_tuple(L):
        return tuple(handle_item(x) for x in L)

    def foo(*args, **kwargs):
        items_cache = make_tuple(sorted(kwargs.items()))
        args_cache = make_tuple(args)
        if (args_cache, items_cache) not in foo.past_calls:
            foo.past_calls[(args_cache, items_cache)] = fn(*args,**kwargs)
        return foo.past_calls[(args_cache, items_cache)]
    foo.past_calls = {}
    foo.__name__ = 'memoized_' + fn.__name__
    return foo

Note that this approach can be naturally extended to any object by implementing your own hash function as a special case in handle_item. For example, to make this approach work for a function that takes a set as an input argument, you could add to handle_item:

if is_instance(x, set):
    return make_tuple(sorted(list(x)))
RussellStewart
  • 4,881
  • 2
  • 23
  • 23
  • 1
    Nice attempt. Without whining, a `list` argument of `[1, 2, 3]` can mistakenly be considered the same as a different `set` argument with a value of `{1, 2, 3}`. In addition, sets are unordered like dictionaries, so they would also need to be `sorted()`. Also note that a recursive data structure argument would cause an infinite loop. – martineau Jan 20 '14 at 01:31
  • Yea, sets should be handled by special casing handle_item(x) and sorting. I shouldn't have said that this implementation handles sets, because it doesn't - but the point is that it can be easily extended to do so by special casing handle_item, and the same will work for any class or iterable object as long as you're willing to write the hash function yourself. The tricky part - dealing with multi-dimensional lists or dictionaries - is already dealt with here, so I've found that this memoize function is a lot easier to work with as a base than the simple "I only take hashable arguments" types. – RussellStewart Jan 21 '14 at 01:36
  • The problem I mentioned is due to the fact that `list`s and `set`s are "tupleized" into the same thing and therefore become indistinguishable from one another. The example code for adding support for `sets` described in your latest update doesn't avoid that I'm afraid. This can easily be seen by separately passing `[1,2,3]` and `{1,2,3}` as an argument to a "memoize"d test function and seeing whether it's called twice, as it should be, or not. – martineau Jan 21 '14 at 02:07
  • yea, I read that problem, but I didn't address it because I think it is much more minor than the other one you mentioned. When was the last time you wrote a memoized function where a fixed argument could be either a list or a set, and the two resulted in different outputs? If you were to run into such a rare case, you would again just rewrite handle_item to prepend, say a 0 if the element is a set, or a 1 if it is a list. – RussellStewart Jan 22 '14 at 00:14
  • Actually, there's a similar issue with `list`s and `dict`s because it's _possible_ for a `list` to have exactly the same thing in it that resulted from calling `make_tuple(sorted(x.items()))` for a dictionary. A simple solution for both cases would be to include the `type()` of value in the tuple generated. I can think of an even simpler way specifically to handle `set`s, but it doesn't generalize. – martineau Jan 22 '14 at 02:49
3

Solution that works with both positional and keyword arguments independently of order in which keyword args were passed (using inspect.getargspec):

import inspect
import functools

def memoize(fn):
    cache = fn.cache = {}
    @functools.wraps(fn)
    def memoizer(*args, **kwargs):
        kwargs.update(dict(zip(inspect.getargspec(fn).args, args)))
        key = tuple(kwargs.get(k, None) for k in inspect.getargspec(fn).args)
        if key not in cache:
            cache[key] = fn(**kwargs)
        return cache[key]
    return memoizer

Similar question: Identifying equivalent varargs function calls for memoization in Python

Community
  • 1
  • 1
ndpu
  • 19,758
  • 4
  • 48
  • 66
3

Just wanted to add to the answers already provided, the Python decorator library has some simple yet useful implementations that can also memoize "unhashable types", unlike functools.lru_cache.

Sid
  • 5,003
  • 2
  • 12
  • 17
  • 2
    This decorator does not *memoize "unhashable types"*! It just falls back to calling the function without memoization, going against against the *explicit is better than implicit* dogma. – ostrokach Jun 01 '16 at 19:47
2
cache = {}
def fib(n):
    if n <= 1:
        return n
    else:
        if n not in cache:
            cache[n] = fib(n-1) + fib(n-2)
        return cache[n]
Vikrant Singh
  • 597
  • 1
  • 6
  • 16
  • 5
    you could use simply `if n not in cache` instead. using `cache.keys` would build an unnecessary list in python 2 – n611x007 Jan 29 '13 at 09:53