Skip to main content

Coverage Status

atools

Python 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.

@memoize

Decorates a function call and caches return value for given inputs.

  • If db_path is provided, memos will persist on disk and reloaded during initialization.
  • If duration is provided, memos will only be valid for given duration.
  • If keygen is provided, memo hash keys will be created with given keygen.
  • If pickler is provided, persistent memos will (de)serialize using given pickler.
  • If size is provided, LRU memo will be evicted if current count exceeds given size.

Examples

  • Body will run once for unique input bar and result is cached.

    @memoize
    def foo(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.

    @memoize
    async def foo(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.
    await asyncio.gather(*[foo(1) for _ in range(10)])
    
  • Classes may be memoized.

    @memoize
    Class Foo:
        def init(self, _): ...
    
    Foo(1)  # Instance is actually created.
    Foo(1)  # Instance not created. Cached instance returned.
    Foo(2)  # Instance is actually created.
    
  • Calls foo(1), foo(bar=1), and foo(1, baz='baz') are equivalent and only cached once.

    @memoize
    def foo(bar, baz='baz'): ...
    
  • Only 2 items are cached. Acts as an LRU.

    @memoize(size=2)
    def foo(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 2
    
  • Items are evicted after 1 minute.

    @memoize(duration=datetime.timedelta(minutes=1))
    def foo(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 .memoize attribute

    @memoize
    def foo(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 .memoize attribute

    @memoize
    def foo(bar) -> Any: ...
    
    foo(1)
    foo(2)
    len(foo.memoize)  # returns 2
    
  • Alternate memo hash function can be specified. The inputs must match the function's.

    Class Foo:
        @memoize(keygen=lambda self, a, b, c: (a, b, c))  # Omit 'self' from hash key.
        def bar(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.

    async def awaitable_key_part() -> Hashable: ...
    
    @memoize(keygen=lambda bar: (bar, awaitable_key_part()))
    async def foo(bar) -> Any: ...
    
  • If the memoized function is async and any part of the key is awaitable, it is awaited.

    async def morph_a(a: int) -> int: ...
    
    @memoize(keygen=lambda a, b, c: (morph_a(a), b, c))
    def foo(a, b, c) -> Any: ...
    
  • Properties can be memoized.

    Class Foo:
        @property
        @memoize
        def bar(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.

    Class Foo:
        @memoize(size=1)
        def bar(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 evicted
    a.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')
    def foo(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)
    def foo(a) -> Any: ...
    
    @memoize_db(duration=datetime.timedelta(hours=1))
    def bar(b) -> Any: ...
    
  • Comparison equality does not affect memoize. Only hash equality matters.

    # Inherits object.__hash__
    class Foo:
        # Don't be fooled. memoize only cares about the hash.
        def __eq__(self, other: Foo) -> bool:
            return True
    
    @memoize
    def bar(foo: Foo) -> Any: ...
    
    foo0, foo1 = Foo(), Foo()
    assert foo0 == foo1
    bar(foo0)  # Function called. Result cached.
    bar(foo1)  # Function called again, despite equality, due to different hash.
    

A warning about arguments that inherit object.__hash__:

It doesn't make sense to keep a memo if it's impossible to generate the same input again. Inputs that inherit the default object.__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, memoize will drop memos created using those inputs.

  • Memo lifetime is bound to the lifetime of any arguments that inherit object.__hash__.

    # Inherits object.__hash__
    class Foo:
        ...
    
    @memoize
    def bar(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.
    del foo  # Memo is deleted at the same time as foo.
    
  • Types that have specific, consistent hash functions (int, str, etc.) won't cause problems.

    @memoize
    def foo(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 from object.__hash__. However, classes are almost never garbage collected until a process exits so memoize will work as expected.

    class Foo:
      @classmethod
      @memoize
      def bar(cls) -> Any: ...
    
    foo = Foo()
    foo.bar()  # Function called. Result cached.
    foo.bar()  # Function not called. Cached result returned.
    
    del foo  # 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 from object.__hash__.

    class Foo:
    
        @memoize
        def bar(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.

    import dill
    
    @memoize(db_path='~/.memoize`, pickler=dill)
    def foo() -> Callable[[], None]:
        return lambda: None
    

rate

Function decorator that rate limits the number of calls to function.

  • size must be provided. It specifies the maximum number of calls that may be made concurrently and optionally within a given duration time window.
  • If duration is provided it limits the maximum call count to size in any given duration time window.

Examples

  • Only 2 concurrent calls allowed.

    @rate(size=2)
    def foo(): ...
    
  • Only 2 calls allowed per minute.

    @rate(size=2, duration=60)
    def foo(): ...
    
  • Same as above, but duration specified with a timedelta.

    @rate(size=2, duration=datetime.timedelta(minutes=1))
    def foo(): ...
    
  • Same as above, but async.

    @rate(size=2, duration=datetime.timedelta(minutes=1))
    async def foo(): ...
    
  • 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)
    def foo(): ...
    

Release files for atools 0.14.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for atools 0.14.2
File Size Uploaded
atools-0.14.2.tar.gz 14.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for atools 0.14.2
File Interpreter ABI Platform
atools-0.14.2-py3-none-any.whl Python 3 none any Details

Total release size:27.4 kB

Release files / atools-0.14.2.tar.gz

Download URL atools-0.14.2.tar.gz
Size 14.9 kB
Tags Source
SHA-256 checksum
How to use checksums
ae2e518fb51355a03c1564b5ba80c7a7d28d1fc3e465fa391bde56e5582e8463
BLAKE2b-256 checksum
How to use checksums
1257288048acd95bbaa4a16ab4d11c2faa9a802ec4b57f83bd634d1711d222b4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/4.0.2 CPython/3.10.7

Release files / atools-0.14.2-py3-none-any.whl

Download URL atools-0.14.2-py3-none-any.whl
Size 12.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
5abdcdd79b1377856fa441e427ccfe55f7caf7c8d0c4e67adeb493c4d5f7f931
BLAKE2b-256 checksum
How to use checksums
0869296815ee731ec01f40dce6c16fc855f26e077ac3d46e0b7638e99f4f1870
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/4.0.2 CPython/3.10.7

Release history Release notifications | RSS feed

This release

0.14.2 This release

2 release files

0.14.1

1 release file

0.14.0

2 release files

0.13.1

1 release file

0.13.0

1 release file

0.12.1

1 release file

0.12.0

1 release file

0.11.0

1 release file

0.10.6

1 release file

0.10.5

1 release file

0.10.4

1 release file

0.10.3

1 release file

0.10.2

1 release file

0.10.1

1 release file

0.10.0

1 release file

0.9.2

1 release file

0.9.1

1 release file

0.9.0

1 release file

0.8.0

1 release file

0.7.0

1 release file

0.6.2

1 release file

0.6.1

1 release file

0.6.0

1 release file

0.5.2

1 release file

0.5.1

1 release file

0.5.0

1 release file

0.4.1

1 release file

0.4.0

1 release file

0.3.1

1 release file

0.3.0

1 release file

0.2.1

1 release file

0.2.0

1 release file

0.1.2

1 release file

0.1.1

1 release file

0.1.0

1 release file

0.0.0

1 release file

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page