# 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"
}