Welcome, guest | Sign In | My Account | Store | Cart
1

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, 26 lines
 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")

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

4 comments

Bartlomiej G√≥rny 12 years, 1 month ago  # | flag

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?

Ori Peleg (author) 12 years, 1 month ago  # | flag

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.

Carsten Milkau 4 years, 10 months ago  # | flag

The only reason your decorator fails for methods is that it lacks a proper __get__() method. I reimplemented your pattern such that it "properly" supports methods. What is proper is of course arguable. For instance, this implementation will run unbound method calls only once (even when in fact the instance argument differs), but bound methods will be called once per instance (and bound classmethods would be called once per class).

class once(object):
    __slots__ = ("func", "result", "methods")
    def __init__(self, func):
        self.func = func
    def __call__(self, *args, **kw):
        try:
            return self.result
        except AttributeError:
            self.result = self.func(*args, **kw)
            return self.result
    def __get__(self, instance, cls):
        method = self.func.__get__(instance, cls)
        try:
            return self.methods[method]
        except (AttributeError,KeyError):
            decorated = once(method)
            try:
                self.methods[method] = decorated
            except AttributeError:
                self.methods = { method : decorated }
            return decorated
    def __eq__(self, other):
        return isinstance(other, once) and other.func == self.func
    def __hash__(self):
        return hash(self.func)

Example session:

>>> from util import once
>>> from random import random
>>> class A(object):
...     @once
...     def foo(self):
...             return random()
... 
>>> A.foo(A())
0.020489987913656083
>>> A.foo(A())
0.020489987913656083
>>> A.foo(A())
0.020489987913656083
>>> a=A()
>>> a.foo()
0.21235968529156313
>>> a.foo()
0.21235968529156313
>>> b=A()
>>> b.foo()
0.7689372869176068
>>> b.foo()
0.7689372869176068
>>> class B(A):
...     pass
... 
>>> B.foo(B())
0.020489987913656083
>>> B.foo(B())
0.020489987913656083
>>> b=B()
>>> b.foo()
0.6263909828046167
>>> b.foo()
0.6263909828046167
>>> B().foo()
0.1537503764598559
>>> B().foo()
0.13707780635986466
Carsten Milkau 4 years, 10 months ago  # | flag

Important: I noticed it is vital to use a WeakKeyDictionary instead of an ordinary dict for once.methods, otherwise all instances live as long as their class (because they are still reachable via cls.decoratedmethod.methods.keys()[someindex].im_self). Fortunately, the __get__ methods of builtin objects seem to memoize their outputs, so the weak keys should live long enough such that bound methods will be really called only once. The once decorator itself also preserves that memoization by design. However, if you use the once decorator on top of non-standard decorators, and they create throw-away instances in their __get__ methods, there is no way to properly detect when a key can be dropped. In that case, you can only use specialized decorators for different method types, or live with the memory leak induced by an ordinary dict, or use a full memoization.

Add a comment

Sign in to comment

Created by Ori Peleg on Sat, 11 Jun 2005 (PSF)
Python recipes (4591)
Ori Peleg's recipes (15)

Required Modules

Other Information and Tasks