Package: atools
atoolsPython 3.6+ decorators including@memoize- a function decorator for sync and async functions that memoizes results.@rate- a function decorator for sync and async functions that rate limits calls.@memoizeDecorates a function call and caches return value for given inputs.Ifdb_pathis provided, memos will persist on disk and reloaded during initialization.Ifdurationis provided, memos will only be valid for givenduration.Ifkeygenis provided, memo hash keys will be created with givenkeygen.Ifpickleris provided, persistent memos will (de)serialize using givenpickler.Ifsizeis provided, LRU memo will be evicted if current count exceeds givensize.ExamplesBody will run once for unique inputbarand result is cached.@memoizedeffoo(bar)->Any:...foo(1)# Function actually called. Result cached.foo(1)# Function not called. Cached result returned.foo(2)# Function actually called. Result cached.Same as above, but async.@memoizeasyncdeffoo(bar)->Any:...# Concurrent calls from the same event loop are safe. Only one call is generated. The# other nine calls in this example wait for the result.awaitasyncio.gather(*[foo(1)for_inrange(10)])Classes may be memoized.@memoizeClassFoo:definit(self,_):...Foo(1)# Instance is actually created.Foo(1)# Instance not created. Cached instance returned.Foo(2)# Instance is actually created.Callsfoo(1),foo(bar=1), andfoo(1, baz='baz')are equivalent and only cached once.@memoizedeffoo(bar,baz='baz'):...Only 2 items are cached. Acts as an LRU.@memoize(size=2)deffoo(bar)->Any:...foo(1)# LRU cache order [foo(1)]foo(2)# LRU cache order [foo(1), foo(2)]foo(1)# LRU cache order [foo(2), foo(1)]foo(3)# LRU cache order [foo(1), foo(3)], foo(2) is evicted to keep cache size at 2Items are evicted after 1 minute.@memoize(duration=datetime.timedelta(minutes=1))deffoo(bar)->Any:...foo(1)# Function actually called. Result cached.foo(1)# Function not called. Cached result returned.sleep(61)foo(1)# Function actually called. Cached result was too old.Memoize can be explicitly reset through the function's.memoizeattribute@memoizedeffoo(bar)->Any:...foo(1)# Function actually called. Result cached.foo(1)# Function not called. Cached result returned.foo.memoize.reset()foo(1)# Function actually called. Cache was emptied.Current cache length can be accessed through the function's.memoizeattribute@memoizedeffoo(bar)->Any:...foo(1)foo(2)len(foo.memoize)# returns 2Alternate memo hash function can be specified. The inputs must match the function's.ClassFoo:@memoize(keygen=lambdaself,a,b,c:(a,b,c))# Omit 'self' from hash key.defbar(self,a,b,c)->Any:...a,b=Foo(),Foo()# Hash key will be (a, b, c)a.bar(1,2,3)# LRU cache order [Foo.bar(a, 1, 2, 3)]# Hash key will again be (a, b, c)# Be aware, in this example the returned result comes from a.bar(...), not b.bar(...).b.bar(1,2,3)# Function not called. Cached result returned.If part of the returned key from keygen is awaitable, it will be awaited.asyncdefawaitable_key_part()->Hashable:...@memoize(keygen=lambdabar:(bar,awaitable_key_part()))asyncdeffoo(bar)->Any:...If the memoized function is async and any part of the key is awaitable, it is awaited.asyncdefmorph_a(a:int)->int:...@memoize(keygen=lambdaa,b,c:(morph_a(a),b,c))deffoo(a,b,c)->Any:...Properties can be memoized.ClassFoo:@property@memoizedefbar(self)->Any:...a=Foo()a.bar# Function actually called. Result cached.a.bar# Function not called. Cached result returned.b=Foo()# Memoize uses 'self' parameter in hash. 'b' does not share returns with 'a'b.bar# Function actually called. Result cached.b.bar# Function not called. Cached result returned.Be careful with eviction on instance methods. Memoize is not instance-specific.ClassFoo:@memoize(size=1)defbar(self,baz)->Any:...a,b=Foo(),Foo()a.bar(1)# LRU cache order [Foo.bar(a, 1)]b.bar(1)# LRU cache order [Foo.bar(b, 1)], Foo.bar(a, 1) is evicteda.bar(1)# Foo.bar(a, 1) is actually called and cached again.Values can persist to disk and be reloaded when memoize is initialized again.@memoize(db_path=Path.home()/'.memoize')deffoo(a)->Any:...foo(1)# Function actually called. Result cached.# Process is restarted. Upon restart, the state of the memoize decorator is reloaded.foo(1)# Function not called. Cached result returned.If not applied to a function, calling the decorator returns a partial application.memoize_db=memoize(db_path=Path.home()/'.memoize')@memoize_db(size=1)deffoo(a)->Any:...@memoize_db(duration=datetime.timedelta(hours=1))defbar(b)->Any:...Comparison equality does not affect memoize. Only hash equality matters.# Inherits object.__hash__classFoo:# Don't be fooled. memoize only cares about the hash.def__eq__(self,other:Foo)->bool:returnTrue@memoizedefbar(foo:Foo)->Any:...foo0,foo1=Foo(),Foo()assertfoo0==foo1bar(foo0)# Function called. Result cached.bar(foo1)# Function called again, despite equality, due to different hash.A warning about arguments that inheritobject.__hash__:It doesn't make sense to keep a memo if it's impossible to generate the same input again. Inputs
that inherit the defaultobject.__hash__are unique based on their id, and thus, their
location in memory. If such inputs are garbage-collected, they are gone forever. For that
reason, when those inputs are garbage collected,memoizewill drop memos created using those
inputs.Memo lifetime is bound to the lifetime of any arguments that inheritobject.__hash__.# Inherits object.__hash__classFoo:...@memoizedefbar(foo:Foo)->Any:...bar(Foo())# Memo is immediately deleted since Foo() is garbage collected.foo=Foo()bar(foo)# Memo isn't deleted until foo is deleted.delfoo# Memo is deleted at the same time as foo.Types that have specific, consistent hash functions (int, str, etc.) won't cause problems.@memoizedeffoo(a:int,b:str,c:Tuple[int,...],d:range)->Any:...foo(1,'bar',(1,2,3),range(42))# Function called. Result cached.foo(1,'bar',(1,2,3),range(42))# Function not called. Cached result returned.Classmethods rely on classes, which inherit fromobject.__hash__. However, classes are
almost never garbage collected until a process exits so memoize will work as expected.classFoo:@classmethod@memoizedefbar(cls)->Any:...foo=Foo()foo.bar()# Function called. Result cached.foo.bar()# Function not called. Cached result returned.delfoo# Memo not cleared since lifetime is bound to class Foo.foo=Foo()foo.bar()# Function not called. Cached result returned.foo.bar()# Function not called. Cached result returned.Long-lasting object instances that inherit fromobject.__hash__.classFoo:@memoizedefbar(self)->Any:...foo=Foo()foo.bar()# Function called. Result cached.# foo instance is kept around somewhere and used later.foo.bar()# Function not called. Cached result returned.Custom pickler may be specified for persistent memo (de)serialization.importdill@memoize(db_path='~/.memoize`, pickler=dill)deffoo()->Callable[[],None]:returnlambda:NonerateFunction decorator that rate limits the number of calls to function.sizemust be provided. It specifies the maximum number of calls that may be made
concurrently and optionally within a givendurationtime window.Ifdurationis provided it limits the maximum call count tosizein any givendurationtime window.ExamplesOnly 2 concurrent calls allowed.@rate(size=2)deffoo():...Only 2 calls allowed per minute.@rate(size=2,duration=60)deffoo():...Same as above, but duration specified with a timedelta.@rate(size=2,duration=datetime.timedelta(minutes=1))deffoo():...Same as above, but async.@rate(size=2,duration=datetime.timedelta(minutes=1))asyncdeffoo():...More advanced rate limiting is possible by composing multiple rate decorators.# Up to 100 calls per minute, but only 10 concurrent.@rate(size=100,duration=60)@rate(size=10)deffoo():...