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).
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
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.
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).
Example session:
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.