import threading from collections import OrderedDict from typing import Any, Callable, Iterator, MutableMapping, Optional, TypeVar K = TypeVar("K") V = TypeVar("V") class LRUCache(MutableMapping[K, V]): """Thread-safe LRUCache based on an OrderedDict. All dict operations (__getitem__, __setitem__, __contains__) update the priority of the relevant key and take O(1) time. The dict is iterated over in order from the oldest to newest key, which means that a complete pass over the dict should not affect the order of any entries. When a new item is set and the maximum size of the cache is exceeded, the oldest item is dropped and called with ``on_evict(key, value)``. The ``maxsize`` property can be used to view or adjust the capacity of the cache, e.g., ``cache.maxsize = new_size``. """ _cache: "OrderedDict[K, V]" _maxsize: int _lock: threading.RLock _on_evict: Optional[Callable[[K, V], Any]] __slots__ = ("_cache", "_lock", "_maxsize", "_on_evict") def __init__(self, maxsize: int, on_evict: Callable[[K, V], Any] = None): """ Parameters ---------- maxsize : int Integer maximum number of items to hold in the cache. on_evict : callable, optional Function to call like ``on_evict(key, value)`` when items are evicted. """ if not isinstance(maxsize, int): raise TypeError("maxsize must be an integer") if maxsize < 0: raise ValueError("maxsize must be non-negative") self._maxsize = maxsize self._cache = OrderedDict() self._lock = threading.RLock() self._on_evict = on_evict def __getitem__(self, key: K) -> V: # record recent use of the key by moving it to the front of the list with self._lock: value = self._cache[key] self._cache.move_to_end(key) return value def _enforce_size_limit(self, capacity: int) -> None: """Shrink the cache if necessary, evicting the oldest items.""" while len(self._cache) > capacity: key, value = self._cache.popitem(last=False) if self._on_evict is not None: self._on_evict(key, value) def __setitem__(self, key: K, value: V) -> None: with self._lock: if key in self._cache: # insert the new value at the end del self._cache[key] self._cache[key] = value elif self._maxsize: # make room if necessary self._enforce_size_limit(self._maxsize - 1) self._cache[key] = value elif self._on_evict is not None: # not saving, immediately evict self._on_evict(key, value) def __delitem__(self, key: K) -> None: del self._cache[key] def __iter__(self) -> Iterator[K]: # create a list, so accessing the cache during iteration cannot change # the iteration order return iter(list(self._cache)) def __len__(self) -> int: return len(self._cache) @property def maxsize(self) -> int: """Maximum number of items can be held in the cache.""" return self._maxsize @maxsize.setter def maxsize(self, size: int) -> None: """Resize the cache, evicting the oldest items if necessary.""" if size < 0: raise ValueError("maxsize must be non-negative") with self._lock: self._enforce_size_limit(size) self._maxsize = size