Skip to content

Commit 77a9c59

Browse files
committed
Fix #260: Use LFUCache implementation based on Blake Reid's "cacheing" library.
1 parent b1d4eb2 commit 77a9c59

File tree

3 files changed

+65
-9
lines changed

3 files changed

+65
-9
lines changed

src/cachetools/__init__.py

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -170,32 +170,78 @@ def popitem(self):
170170
class LFUCache(Cache):
171171
"""Least Frequently Used (LFU) cache implementation."""
172172

173+
class _Link:
174+
__slots__ = ("count", "keys", "next", "prev")
175+
176+
def __init__(self, count):
177+
self.count = count
178+
self.keys = set()
179+
180+
def unlink(self):
181+
next = self.next
182+
prev = self.prev
183+
prev.next = next
184+
next.prev = prev
185+
173186
def __init__(self, maxsize, getsizeof=None):
174187
Cache.__init__(self, maxsize, getsizeof)
175-
self.__counter = collections.Counter()
188+
self.__root = root = LFUCache._Link(0) # sentinel
189+
root.prev = root.next = root
190+
self.__links = {}
176191

177192
def __getitem__(self, key, cache_getitem=Cache.__getitem__):
178193
value = cache_getitem(self, key)
179194
if key in self: # __missing__ may not store item
180-
self.__counter[key] -= 1
195+
self.__touch(key)
181196
return value
182197

183198
def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
184199
cache_setitem(self, key, value)
185-
self.__counter[key] -= 1
200+
if key in self.__links:
201+
return self.__touch(key)
202+
root = self.__root
203+
link = root.next
204+
if link.count != 1:
205+
link = LFUCache._Link(1)
206+
link.next = root.next
207+
root.next = link.next.prev = link
208+
link.prev = root
209+
link.keys.add(key)
210+
self.__links[key] = link
186211

187212
def __delitem__(self, key, cache_delitem=Cache.__delitem__):
188213
cache_delitem(self, key)
189-
del self.__counter[key]
214+
link = self.__links.pop(key)
215+
link.keys.remove(key)
216+
if not link.keys:
217+
link.unlink()
190218

191219
def popitem(self):
192220
"""Remove and return the `(key, value)` pair least frequently used."""
193-
try:
194-
((key, _),) = self.__counter.most_common(1)
195-
except ValueError:
221+
root = self.__root
222+
curr = root.next
223+
if curr is root:
196224
raise KeyError("%s is empty" % type(self).__name__) from None
197-
else:
198-
return (key, self.pop(key))
225+
key = next(iter(curr.keys)) # remove an arbitrary element
226+
return (key, self.pop(key))
227+
228+
def __touch(self, key):
229+
"""Increment use count"""
230+
link = self.__links[key]
231+
curr = link.next
232+
if curr.count != link.count + 1:
233+
if len(link.keys) == 1:
234+
link.count += 1
235+
return
236+
curr = LFUCache._Link(link.count + 1)
237+
curr.next = link.next
238+
link.next = curr.next.prev = curr
239+
curr.prev = link
240+
curr.keys.add(key)
241+
link.keys.remove(key)
242+
if not link.keys:
243+
link.unlink()
244+
self.__links[key] = curr
199245

200246

201247
class LRUCache(Cache):

tests/test_func.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@ def test_decorator_user_function(self):
7676
self.assertEqual(cached.cache_info(), (2, 1, 128, 1))
7777

7878
def test_decorator_needs_rlock(self):
79+
"""This will deadlock on a cache that uses a regular lock.
80+
https://github.com/python/cpython/blob/3.13/Lib/test/test_functools.py#L1791
81+
82+
"""
83+
7984
cached = self.decorator(lambda n: n)
8085

8186
class RecursiveEquals:

tests/test_lfu.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ def test_lfu(self):
2626
self.assertEqual(cache[4], 4)
2727
self.assertEqual(cache[1], 1)
2828

29+
cache[1]
30+
self.assertEqual(len(cache), 2)
31+
self.assertEqual(cache[1], 1)
32+
self.assertEqual(cache[4], 4)
33+
2934
def test_lfu_getsizeof(self):
3035
cache = LFUCache(maxsize=3, getsizeof=lambda x: x)
3136

0 commit comments

Comments
 (0)