cached-property-with-invalidation


Namecached-property-with-invalidation JSON
Version 0.0.2 PyPI version JSON
download
home_pagehttps://github.com/tylerlum/cached_property_with_invalidation
SummaryDecorator to create cached_property that can be invalidated when invalidation variable is updated
upload_time2023-10-07 23:51:04
maintainer
docs_urlNone
authorTyler Lum
requires_python
license
keywords python cached_property cached property invalidation
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # cached_property_with_invalidation

Decorator to create cached_property that can be invalidated when invalidation variable is updated

# Installing

Install:
```
pip install cached_property_with_invalidation
```

# Usage

Example usage comparing:

1. `cached_property_with_invalidation`

2. `cached_property`

3. `property`

This example demonstrates a common use case in which we have a slow computation that we want to cache temporarily, but we need to invalidate the cache when the class state updates. If we simply use `property`, no caching will be done, which hurts performance. If we simply use `cached_property`, caching will be done, but we will get incorrect values when the class state updates. In contrast, using `cached_property_with_invalidation` allows us to correctly compute the right values when the class state updates, but caches the value when it has not been updated.

This is a very common use-case in physics-based simulation, where we have a simulation physics state that is updated on each simulation step, and we have expensive computations on that physics state we want to cache.

```
from cached_property_with_invalidation import (
    cached_property_with_invalidation,
)
import time


try:
    from functools import cached_property
except ImportError:
    from functools import lru_cache

    def cached_property(func):
        @property
        @lru_cache()
        def wrapped_method(self):
            return func(self)

        return wrapped_method


SLOW_FUNCTION_TIME_MIN_SECONDS = 0.1
CACHE_ACCESS_TIME_MAX_SECONDS = 0.01
INVALIDATION_VARIABLE_NAME = "counter"


class ExampleClass:
    def __init__(self):
        self.counter = 0
        self.internal_state = [i for i in range(10)]

    def update_state(self):
        self.counter += 1
        self.internal_state = [i + 1 for i in self.internal_state]

    def slow_double_internal_state(self):
        time.sleep(SLOW_FUNCTION_TIME_MIN_SECONDS)
        return [i * 2 for i in self.internal_state]

    @cached_property_with_invalidation(INVALIDATION_VARIABLE_NAME)
    def slow_double_internal_state_with_cache_and_invalidation(self):
        return self.slow_double_internal_state()

    @cached_property
    def slow_double_internal_state_with_cache_no_invalidation(self):
        return self.slow_double_internal_state()

    @property
    def slow_double_internal_state_no_cache(self):
        return self.slow_double_internal_state()


def test_with_cache_and_invalidation():
    # Correct behavior and fast
    example_class = ExampleClass()

    t0 = time.time()
    output0 = example_class.slow_double_internal_state_with_cache_and_invalidation
    t1 = time.time()
    cached_output0 = (
        example_class.slow_double_internal_state_with_cache_and_invalidation
    )
    t2 = time.time()

    example_class.update_state()
    t3 = time.time()
    output1 = example_class.slow_double_internal_state_with_cache_and_invalidation
    t4 = time.time()
    cached_output1 = (
        example_class.slow_double_internal_state_with_cache_and_invalidation
    )
    t5 = time.time()

    compute_output0_time = t1 - t0
    compute_cached_output0_time = t2 - t1
    compute_output1_time = t4 - t3
    compute_cached_output1_time = t5 - t4

    assert output0 == cached_output0
    assert output0 != output1
    assert output1 == cached_output1

    assert compute_output0_time > SLOW_FUNCTION_TIME_MIN_SECONDS
    assert compute_cached_output0_time < CACHE_ACCESS_TIME_MAX_SECONDS
    assert compute_output1_time > SLOW_FUNCTION_TIME_MIN_SECONDS
    assert compute_cached_output1_time < CACHE_ACCESS_TIME_MAX_SECONDS


def test_with_cache_no_invalidation():
    # Fast but incorrect behavior
    example_class = ExampleClass()

    t0 = time.time()
    output0 = example_class.slow_double_internal_state_with_cache_no_invalidation
    t1 = time.time()
    cached_output0 = example_class.slow_double_internal_state_with_cache_no_invalidation
    t2 = time.time()

    example_class.update_state()
    t3 = time.time()
    output1 = example_class.slow_double_internal_state_with_cache_no_invalidation
    t4 = time.time()
    cached_output1 = example_class.slow_double_internal_state_with_cache_no_invalidation
    t5 = time.time()

    compute_output0_time = t1 - t0
    compute_cached_output0_time = t2 - t1
    compute_output1_time = t4 - t3
    compute_cached_output1_time = t5 - t4

    assert output0 == cached_output0
    assert output0 == output1
    assert output1 == cached_output1

    assert compute_output0_time > SLOW_FUNCTION_TIME_MIN_SECONDS
    assert compute_cached_output0_time < CACHE_ACCESS_TIME_MAX_SECONDS
    assert compute_output1_time < CACHE_ACCESS_TIME_MAX_SECONDS
    assert compute_cached_output1_time < CACHE_ACCESS_TIME_MAX_SECONDS


def test_no_cache():
    # Correct behavior but slow
    example_class = ExampleClass()

    t0 = time.time()
    output0 = example_class.slow_double_internal_state_no_cache
    t1 = time.time()
    uncached_output0 = example_class.slow_double_internal_state_no_cache
    t2 = time.time()

    example_class.update_state()
    t3 = time.time()
    output1 = example_class.slow_double_internal_state_no_cache
    t4 = time.time()
    uncached_output1 = example_class.slow_double_internal_state_no_cache
    t5 = time.time()

    compute_output0_time = t1 - t0
    compute_uncached_output0_time = t2 - t1
    compute_output1_time = t4 - t3
    compute_uncached_output1_time = t5 - t4

    assert output0 == uncached_output0
    assert output0 != output1
    assert output1 == uncached_output1

    assert compute_output0_time > SLOW_FUNCTION_TIME_MIN_SECONDS
    assert compute_uncached_output0_time > SLOW_FUNCTION_TIME_MIN_SECONDS
    assert compute_output1_time > SLOW_FUNCTION_TIME_MIN_SECONDS
    assert compute_uncached_output1_time > SLOW_FUNCTION_TIME_MIN_SECONDS


test_with_cache_and_invalidation()
test_with_cache_no_invalidation()
test_no_cache()
```

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/tylerlum/cached_property_with_invalidation",
    "name": "cached-property-with-invalidation",
    "maintainer": "",
    "docs_url": null,
    "requires_python": "",
    "maintainer_email": "",
    "keywords": "python,cached_property,cached,property,invalidation",
    "author": "Tyler Lum",
    "author_email": "tylergwlum@gmail.com",
    "download_url": "https://files.pythonhosted.org/packages/8d/67/849a975f6ea523db688870d46281de68923760399ddcbc7db1a26eefc120/cached_property_with_invalidation-0.0.2.tar.gz",
    "platform": null,
    "description": "# cached_property_with_invalidation\n\nDecorator to create cached_property that can be invalidated when invalidation variable is updated\n\n# Installing\n\nInstall:\n```\npip install cached_property_with_invalidation\n```\n\n# Usage\n\nExample usage comparing:\n\n1. `cached_property_with_invalidation`\n\n2. `cached_property`\n\n3. `property`\n\nThis example demonstrates a common use case in which we have a slow computation that we want to cache temporarily, but we need to invalidate the cache when the class state updates. If we simply use `property`, no caching will be done, which hurts performance. If we simply use `cached_property`, caching will be done, but we will get incorrect values when the class state updates. In contrast, using `cached_property_with_invalidation` allows us to correctly compute the right values when the class state updates, but caches the value when it has not been updated.\n\nThis is a very common use-case in physics-based simulation, where we have a simulation physics state that is updated on each simulation step, and we have expensive computations on that physics state we want to cache.\n\n```\nfrom cached_property_with_invalidation import (\n    cached_property_with_invalidation,\n)\nimport time\n\n\ntry:\n    from functools import cached_property\nexcept ImportError:\n    from functools import lru_cache\n\n    def cached_property(func):\n        @property\n        @lru_cache()\n        def wrapped_method(self):\n            return func(self)\n\n        return wrapped_method\n\n\nSLOW_FUNCTION_TIME_MIN_SECONDS = 0.1\nCACHE_ACCESS_TIME_MAX_SECONDS = 0.01\nINVALIDATION_VARIABLE_NAME = \"counter\"\n\n\nclass ExampleClass:\n    def __init__(self):\n        self.counter = 0\n        self.internal_state = [i for i in range(10)]\n\n    def update_state(self):\n        self.counter += 1\n        self.internal_state = [i + 1 for i in self.internal_state]\n\n    def slow_double_internal_state(self):\n        time.sleep(SLOW_FUNCTION_TIME_MIN_SECONDS)\n        return [i * 2 for i in self.internal_state]\n\n    @cached_property_with_invalidation(INVALIDATION_VARIABLE_NAME)\n    def slow_double_internal_state_with_cache_and_invalidation(self):\n        return self.slow_double_internal_state()\n\n    @cached_property\n    def slow_double_internal_state_with_cache_no_invalidation(self):\n        return self.slow_double_internal_state()\n\n    @property\n    def slow_double_internal_state_no_cache(self):\n        return self.slow_double_internal_state()\n\n\ndef test_with_cache_and_invalidation():\n    # Correct behavior and fast\n    example_class = ExampleClass()\n\n    t0 = time.time()\n    output0 = example_class.slow_double_internal_state_with_cache_and_invalidation\n    t1 = time.time()\n    cached_output0 = (\n        example_class.slow_double_internal_state_with_cache_and_invalidation\n    )\n    t2 = time.time()\n\n    example_class.update_state()\n    t3 = time.time()\n    output1 = example_class.slow_double_internal_state_with_cache_and_invalidation\n    t4 = time.time()\n    cached_output1 = (\n        example_class.slow_double_internal_state_with_cache_and_invalidation\n    )\n    t5 = time.time()\n\n    compute_output0_time = t1 - t0\n    compute_cached_output0_time = t2 - t1\n    compute_output1_time = t4 - t3\n    compute_cached_output1_time = t5 - t4\n\n    assert output0 == cached_output0\n    assert output0 != output1\n    assert output1 == cached_output1\n\n    assert compute_output0_time > SLOW_FUNCTION_TIME_MIN_SECONDS\n    assert compute_cached_output0_time < CACHE_ACCESS_TIME_MAX_SECONDS\n    assert compute_output1_time > SLOW_FUNCTION_TIME_MIN_SECONDS\n    assert compute_cached_output1_time < CACHE_ACCESS_TIME_MAX_SECONDS\n\n\ndef test_with_cache_no_invalidation():\n    # Fast but incorrect behavior\n    example_class = ExampleClass()\n\n    t0 = time.time()\n    output0 = example_class.slow_double_internal_state_with_cache_no_invalidation\n    t1 = time.time()\n    cached_output0 = example_class.slow_double_internal_state_with_cache_no_invalidation\n    t2 = time.time()\n\n    example_class.update_state()\n    t3 = time.time()\n    output1 = example_class.slow_double_internal_state_with_cache_no_invalidation\n    t4 = time.time()\n    cached_output1 = example_class.slow_double_internal_state_with_cache_no_invalidation\n    t5 = time.time()\n\n    compute_output0_time = t1 - t0\n    compute_cached_output0_time = t2 - t1\n    compute_output1_time = t4 - t3\n    compute_cached_output1_time = t5 - t4\n\n    assert output0 == cached_output0\n    assert output0 == output1\n    assert output1 == cached_output1\n\n    assert compute_output0_time > SLOW_FUNCTION_TIME_MIN_SECONDS\n    assert compute_cached_output0_time < CACHE_ACCESS_TIME_MAX_SECONDS\n    assert compute_output1_time < CACHE_ACCESS_TIME_MAX_SECONDS\n    assert compute_cached_output1_time < CACHE_ACCESS_TIME_MAX_SECONDS\n\n\ndef test_no_cache():\n    # Correct behavior but slow\n    example_class = ExampleClass()\n\n    t0 = time.time()\n    output0 = example_class.slow_double_internal_state_no_cache\n    t1 = time.time()\n    uncached_output0 = example_class.slow_double_internal_state_no_cache\n    t2 = time.time()\n\n    example_class.update_state()\n    t3 = time.time()\n    output1 = example_class.slow_double_internal_state_no_cache\n    t4 = time.time()\n    uncached_output1 = example_class.slow_double_internal_state_no_cache\n    t5 = time.time()\n\n    compute_output0_time = t1 - t0\n    compute_uncached_output0_time = t2 - t1\n    compute_output1_time = t4 - t3\n    compute_uncached_output1_time = t5 - t4\n\n    assert output0 == uncached_output0\n    assert output0 != output1\n    assert output1 == uncached_output1\n\n    assert compute_output0_time > SLOW_FUNCTION_TIME_MIN_SECONDS\n    assert compute_uncached_output0_time > SLOW_FUNCTION_TIME_MIN_SECONDS\n    assert compute_output1_time > SLOW_FUNCTION_TIME_MIN_SECONDS\n    assert compute_uncached_output1_time > SLOW_FUNCTION_TIME_MIN_SECONDS\n\n\ntest_with_cache_and_invalidation()\ntest_with_cache_no_invalidation()\ntest_no_cache()\n```\n",
    "bugtrack_url": null,
    "license": "",
    "summary": "Decorator to create cached_property that can be invalidated when invalidation variable is updated",
    "version": "0.0.2",
    "project_urls": {
        "Homepage": "https://github.com/tylerlum/cached_property_with_invalidation"
    },
    "split_keywords": [
        "python",
        "cached_property",
        "cached",
        "property",
        "invalidation"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "fef6b2f29a3bf238ccea8516a25979d8fa67f72adb0bb6be9a39f9b7a32176fc",
                "md5": "8925f9a30771e674dd114bc5ff183e26",
                "sha256": "e51652b0b7bf5a766e6f3fa1a8e5df522052d1fcdd74bf22c25b54b21def830e"
            },
            "downloads": -1,
            "filename": "cached_property_with_invalidation-0.0.2-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "8925f9a30771e674dd114bc5ff183e26",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": null,
            "size": 4748,
            "upload_time": "2023-10-07T23:51:02",
            "upload_time_iso_8601": "2023-10-07T23:51:02.597343Z",
            "url": "https://files.pythonhosted.org/packages/fe/f6/b2f29a3bf238ccea8516a25979d8fa67f72adb0bb6be9a39f9b7a32176fc/cached_property_with_invalidation-0.0.2-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "8d67849a975f6ea523db688870d46281de68923760399ddcbc7db1a26eefc120",
                "md5": "116460bde136304cf9dddcf7f1b1c5f6",
                "sha256": "c0af0f1109ad198e6895972e67fdd847de5393e797be340cb5bc334a10d08e9d"
            },
            "downloads": -1,
            "filename": "cached_property_with_invalidation-0.0.2.tar.gz",
            "has_sig": false,
            "md5_digest": "116460bde136304cf9dddcf7f1b1c5f6",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": null,
            "size": 4172,
            "upload_time": "2023-10-07T23:51:04",
            "upload_time_iso_8601": "2023-10-07T23:51:04.268921Z",
            "url": "https://files.pythonhosted.org/packages/8d/67/849a975f6ea523db688870d46281de68923760399ddcbc7db1a26eefc120/cached_property_with_invalidation-0.0.2.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2023-10-07 23:51:04",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "tylerlum",
    "github_project": "cached_property_with_invalidation",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": false,
    "lcname": "cached-property-with-invalidation"
}
        
Elapsed time: 0.25851s