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
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?
Hmm... 1. I liked your idea, so I reimplemented the recipe
I found and fixed a bug when using @once with a method, now there are two implementations
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