ActiveState Code

Recipe 425445: "once" decorator


This decorator runs a function or method once and caches the result.

It offers minimal memory use and high speed (only one extra function call). It is _not_ a memoization implementation, the result is cached for all future arguments as well.

This code is used in the TestOOB testing framework (http://testoob.sourceforge.net).

Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def func_once(func):
    "A decorator that runs a function only once."
    def decorated(*args, **kwargs):
        try:
            return decorated._once_result
        except AttributeError:
            decorated._once_result = func(*args, **kwargs)
            return decorated._once_result
    return decorated

def method_once(method):
    "A decorator that runs a method only once."
    attrname = "_%s_once_result" % id(method)
    def decorated(self, *args, **kwargs):
        try:
            return getattr(self, attrname)
        except AttributeError:
            setattr(self, attrname, method(self, *args, **kwargs))
            return getattr(self, attrname)
    return decorated

# Example, will only parse the document once
@func_once
def get_document():
    import xml.dom.minidom
    return xml.dom.minidom.parse("document.xml")

Discussion

This is a lightweight "run once" decorator. With it you don't have to worry about the inefficiency of recomputing functions that should return the same value in a given run. If they prove to be a performance hit, just add @func_once or @method_once.

This is inspired by Tadayoshi Funaba's "once" implemented in Ruby (see http://www.ruby-doc.org/docs/ProgrammingRuby/html/classes.html, search for "ExampleDate.once").

I tried getting the same benefits as his implementation, mainly having no extra function calls at all, by creating a wrapper class and redefining its __call__ method after the first execution, but I could only get it to work for functions -- not methods. At least there's only one call, no extra calls or conditionals.

Memoization is a more general concept than this recipe, with implementations such as: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/325905 http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/325205 http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52201

Update - you can now delete the cached result for functions as follows (only works if the once decorator is the only or last decorator applied):

@func_once def foo(): ...

foo() # computes foo() # cached del foo._once_result foo() # recomputes

Comments

  1. 1. At 1:10 p.m. on 17 jun 2005, Bartlomiej Górny said:

    Great, but - how to reset? This is great, a real brain-twister :) And very Pythonic at that.

    I wonder how to reset the thing at runtime, so that the function can be re-run in case background data changes - is it possible?

  2. 2. At 4:38 p.m. on 18 jun 2005, Ori Peleg (the author) said:

    Hmm... 1. I liked your idea, so I reimplemented the recipe

    1. I found and fixed a bug when using @once with a method, now there are two implementations

    2. The new implementations are about 2 times faster than the old one

    I think the speedup comes from replacing that extra function call with a cheaper getattr-in-a-try-block. I stopped using the local list to hold the result, even though it is slightly faster, because the code is less readable.

Sign in to comment