Created
August 6, 2025 09:39
-
-
Save nrbnlulu/59fb9fb881887c4c7795cb54938c60f2 to your computer and use it in GitHub Desktop.
cached python method
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| def memoized_method[**Ps, T](hash_fn: Callable[Ps, int] | None = None) -> Callable[[Callable[Ps, T]], Callable[Ps, T]]: | |
| def wrapper(fn: Callable[Ps, T]) -> Callable[Ps, T]: | |
| @functools.wraps(fn) | |
| def ret(self, *args, **kwargs) -> T: | |
| try: | |
| cache: LRUCache[int, T] = self.__memoized_cache__ | |
| except AttributeError: | |
| self.__memoized_cache__ = cache = LRUCache(capacity=100) | |
| hash_key = hash_fn(self, *args, **kwargs) if hash_fn else hash((args, frozendict(kwargs.items()))) | |
| if found := cache.get(hash_key): | |
| return found | |
| result = fn(self, *args, **kwargs) | |
| cache[hash_key] = result | |
| return result | |
| return ret | |
| return wrapper |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import gc | |
| import weakref | |
| from unittest.mock import Mock | |
| from core.utils import memoized_method | |
| def test_memoized_method_caches_results() -> None: | |
| """Test that memoized_method caches results for repeated calls.""" | |
| mock_obj = Mock() | |
| call_count = 0 | |
| @memoized_method() | |
| def expensive_method(self, x: int, y: int) -> int: | |
| nonlocal call_count | |
| call_count += 1 | |
| return x + y | |
| # First call should execute the method | |
| result1 = expensive_method(mock_obj, 1, 2) | |
| assert result1 == 3 | |
| assert call_count == 1 | |
| # Second call with same args should return cached result | |
| result2 = expensive_method(mock_obj, 1, 2) | |
| assert result2 == 3 | |
| assert call_count == 1 # Should not increment | |
| # Call with different args should execute the method again | |
| result3 = expensive_method(mock_obj, 2, 3) | |
| assert result3 == 5 | |
| assert call_count == 2 | |
| def test_memoized_method_with_kwargs() -> None: | |
| """Test that memoized_method works with keyword arguments.""" | |
| mock_obj = Mock() | |
| call_count = 0 | |
| @memoized_method() | |
| def method_with_kwargs(self, x: int, y: int = 10, z: str = "default") -> str: | |
| nonlocal call_count | |
| call_count += 1 | |
| return f"{x}-{y}-{z}" | |
| # First call | |
| result1 = method_with_kwargs(mock_obj, 1, y=20, z="test") | |
| assert result1 == "1-20-test" | |
| assert call_count == 1 | |
| # Same call should be cached | |
| result2 = method_with_kwargs(mock_obj, 1, y=20, z="test") | |
| assert result2 == "1-20-test" | |
| assert call_count == 1 | |
| # Different kwargs should not be cached | |
| result3 = method_with_kwargs(mock_obj, 1, y=30, z="test") | |
| assert result3 == "1-30-test" | |
| assert call_count == 2 | |
| def test_memoized_method_with_custom_hash_function() -> None: | |
| """Test that memoized_method works with a custom hash function.""" | |
| mock_obj = Mock() | |
| call_count = 0 | |
| def custom_hash(self, x: int, y: int) -> int: | |
| # Custom hash that only considers x, ignoring y | |
| return hash(x) | |
| @memoized_method(hash_fn=custom_hash) | |
| def method_with_custom_hash(self, x: int, y: int) -> int: | |
| nonlocal call_count | |
| call_count += 1 | |
| return x * y | |
| # First call | |
| result1 = method_with_custom_hash(mock_obj, 2, 3) | |
| assert result1 == 6 | |
| assert call_count == 1 | |
| # Call with same x but different y should return cached result | |
| # because custom hash only considers x | |
| result2 = method_with_custom_hash(mock_obj, 2, 5) | |
| assert result2 == 6 # Returns cached result, not 2*5=10 | |
| assert call_count == 1 | |
| # Call with different x should execute method | |
| result3 = method_with_custom_hash(mock_obj, 3, 4) | |
| assert result3 == 12 | |
| assert call_count == 2 | |
| def test_memoized_method_preserves_function_metadata() -> None: | |
| """Test that memoized_method preserves the original function's metadata.""" | |
| mock_obj = Mock() | |
| @memoized_method() | |
| def documented_method(self, x: int) -> int: | |
| """This is a documented method.""" | |
| return x * 2 | |
| assert documented_method.__name__ == "documented_method" | |
| assert documented_method.__doc__ == "This is a documented method." | |
| def test_memoized_method_separate_instance_caches() -> None: | |
| """Test that different instances have separate caches.""" | |
| mock_obj1 = Mock() | |
| mock_obj2 = Mock() | |
| call_count = 0 | |
| @memoized_method() | |
| def instance_method(self, x: int) -> int: | |
| nonlocal call_count | |
| call_count += 1 | |
| return x * 10 | |
| # Call with first object | |
| result1 = instance_method(mock_obj1, 5) | |
| assert result1 == 50 | |
| assert call_count == 1 | |
| # Call with second object but same args - should execute again | |
| # because each instance has its own cache | |
| result2 = instance_method(mock_obj2, 5) | |
| assert result2 == 50 | |
| assert call_count == 2 # Should increment for different instance | |
| # Call with first object again - should use cached result | |
| result3 = instance_method(mock_obj1, 5) | |
| assert result3 == 50 | |
| assert call_count == 2 # Should not increment | |
| # Call with second object again - should use cached result | |
| result4 = instance_method(mock_obj2, 5) | |
| assert result4 == 50 | |
| assert call_count == 2 # Should not increment | |
| def test_memoized_method_garbage_collection() -> None: | |
| """Test that the cache doesn't prevent garbage collection of instances.""" | |
| call_count = 0 | |
| class TestClass: | |
| def __init__(self, value: int): | |
| self.value = value | |
| @memoized_method() | |
| def cached_method(self, x: int) -> int: | |
| nonlocal call_count | |
| call_count += 1 | |
| return self.value + x | |
| # Create an instance and call the cached method | |
| obj = TestClass(10) | |
| weak_ref = weakref.ref(obj) | |
| # Call the method to create cache | |
| result = obj.cached_method(5) | |
| assert result == 15 | |
| assert call_count == 1 | |
| # Verify the object exists | |
| assert weak_ref() is not None | |
| # Delete the reference and force garbage collection | |
| del obj | |
| gc.collect() | |
| # The object should be garbage collected | |
| assert weak_ref() is None |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment