-
Notifications
You must be signed in to change notification settings - Fork 0
/
lfu_cache.py
194 lines (153 loc) · 5.4 KB
/
lfu_cache.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
from __future__ import annotations
from functools import wraps
from typing import Callable
class DoubleLinkedListNode:
"""
Double Linked List Node built specifically for LFU Cache
"""
def __init__(self, key: int, val: int):
self.key = key
self.val = val
self.freq = 0
self.next = None
self.prev = None
class DoubleLinkedList:
"""
Double Linked List built specifically for LFU Cache
"""
def __init__(self):
self.head = DoubleLinkedListNode(None, None)
self.rear = DoubleLinkedListNode(None, None)
self.head.next, self.rear.prev = self.rear, self.head
def add(self, node: DoubleLinkedListNode) -> None:
"""
Adds the given node at the head of the list and shifting it to proper position
"""
temp = self.rear.prev
self.rear.prev, node.next = node, self.rear
node.prev, temp.next = temp, node
node.freq += 1
self._position_node(node)
def _position_node(self, node: DoubleLinkedListNode) -> None:
while node.prev.key and node.prev.freq > node.freq:
node1, node2 = node, node.prev
node1.prev, node2.next = node2.prev, node1.next
node1.next, node2.prev = node2, node1
def remove(self, node: DoubleLinkedListNode) -> DoubleLinkedListNode:
"""
Removes and returns the given node from the list
"""
temp_last, temp_next = node.prev, node.next
node.prev, node.next = None, None
temp_last.next, temp_next.prev = temp_next, temp_last
return node
class LFUCache:
"""
LFU Cache to store a given capacity of data. Can be used as a stand-alone object
or as a function decorator.
>>> cache = LFUCache(2)
>>> cache.set(1, 1)
>>> cache.set(2, 2)
>>> cache.get(1)
1
>>> cache.set(3, 3)
>>> cache.get(2) # None is returned
>>> cache.set(4, 4)
>>> cache.get(1) # None is returned
>>> cache.get(3)
3
>>> cache.get(4)
4
>>> cache
CacheInfo(hits=3, misses=2, capacity=2, current_size=2)
>>> @LFUCache.decorator(100)
... def fib(num):
... if num in (1, 2):
... return 1
... return fib(num - 1) + fib(num - 2)
>>> for i in range(1, 101):
... res = fib(i)
>>> fib.cache_info()
CacheInfo(hits=196, misses=100, capacity=100, current_size=100)
"""
# class variable to map the decorator functions to their respective instance
decorator_function_to_instance_map = {}
def __init__(self, capacity: int):
self.list = DoubleLinkedList()
self.capacity = capacity
self.num_keys = 0
self.hits = 0
self.miss = 0
self.cache = {}
def __repr__(self) -> str:
"""
Return the details for the cache instance
[hits, misses, capacity, current_size]
"""
return (
f"CacheInfo(hits={self.hits}, misses={self.miss}, "
f"capacity={self.capacity}, current_size={self.num_keys})"
)
def __contains__(self, key: int) -> bool:
"""
>>> cache = LFUCache(1)
>>> 1 in cache
False
>>> cache.set(1, 1)
>>> 1 in cache
True
"""
return key in self.cache
def get(self, key: int) -> int | None:
"""
Returns the value for the input key and updates the Double Linked List. Returns
None if key is not present in cache
"""
if key in self.cache:
self.hits += 1
self.list.add(self.list.remove(self.cache[key]))
return self.cache[key].val
self.miss += 1
return None
def set(self, key: int, value: int) -> None:
"""
Sets the value for the input key and updates the Double Linked List
"""
if key not in self.cache:
if self.num_keys >= self.capacity:
key_to_delete = self.list.head.next.key
self.list.remove(self.cache[key_to_delete])
del self.cache[key_to_delete]
self.num_keys -= 1
self.cache[key] = DoubleLinkedListNode(key, value)
self.list.add(self.cache[key])
self.num_keys += 1
else:
node = self.list.remove(self.cache[key])
node.val = value
self.list.add(node)
@staticmethod
def decorator(size: int=128):
"""
Decorator version fo LFU Cache
"""
def cache_decorator_inner(func: Callable):
@wraps(func)
def cache_decorator_wrapper(*args, **kwargs):
if func not in LFUCache.decorator_function_to_instance_map:
LFUCache.decorator_function_to_instance_map[func] = LFUCache(size)
result = LFUCache.decorator_function_to_instance_map[func].get(args[0])
if result is None:
result = func(*args, **kwargs)
LFUCache.decorator_function_to_instance_map[func].set(
args[0], result
)
return result
def cache_info():
return LFUCache.decorator_function_to_instance_map[func]
cache_decorator_wrapper.cache_info = cache_info
return cache_decorator_wrapper
return cache_decorator_inner
if __name__ == "__main__":
import doctest
doctest.testmod()