11from __future__ import annotations
22
3- from typing import Callable
3+ from typing import Callable , Generic , TypeVar
44
5+ T = TypeVar ("T" )
6+ U = TypeVar ("U" )
57
6- class DoubleLinkedListNode :
8+
9+ class DoubleLinkedListNode (Generic [T , U ]):
710 """
811 Double Linked List Node built specifically for LFU Cache
12+
13+ >>> node = DoubleLinkedListNode(1,1)
14+ >>> node
15+ Node: key: 1, val: 1, freq: 0, has next: False, has prev: False
916 """
1017
11- def __init__ (self , key : int , val : int ):
18+ def __init__ (self , key : T | None , val : U | None ):
1219 self .key = key
1320 self .val = val
14- self .freq = 0
15- self .next = None
16- self .prev = None
21+ self .freq : int = 0
22+ self .next : DoubleLinkedListNode [ T , U ] | None = None
23+ self .prev : DoubleLinkedListNode [ T , U ] | None = None
1724
25+ def __repr__ (self ) -> str :
26+ return "Node: key: {}, val: {}, freq: {}, has next: {}, has prev: {}" .format (
27+ self .key , self .val , self .freq , self .next is not None , self .prev is not None
28+ )
1829
19- class DoubleLinkedList :
30+
31+ class DoubleLinkedList (Generic [T , U ]):
2032 """
2133 Double Linked List built specifically for LFU Cache
34+
35+ >>> dll: DoubleLinkedList = DoubleLinkedList()
36+ >>> dll
37+ DoubleLinkedList,
38+ Node: key: None, val: None, freq: 0, has next: True, has prev: False,
39+ Node: key: None, val: None, freq: 0, has next: False, has prev: True
40+
41+ >>> first_node = DoubleLinkedListNode(1,10)
42+ >>> first_node
43+ Node: key: 1, val: 10, freq: 0, has next: False, has prev: False
44+
45+
46+ >>> dll.add(first_node)
47+ >>> dll
48+ DoubleLinkedList,
49+ Node: key: None, val: None, freq: 0, has next: True, has prev: False,
50+ Node: key: 1, val: 10, freq: 1, has next: True, has prev: True,
51+ Node: key: None, val: None, freq: 0, has next: False, has prev: True
52+
53+ >>> # node is mutated
54+ >>> first_node
55+ Node: key: 1, val: 10, freq: 1, has next: True, has prev: True
56+
57+ >>> second_node = DoubleLinkedListNode(2,20)
58+ >>> second_node
59+ Node: key: 2, val: 20, freq: 0, has next: False, has prev: False
60+
61+ >>> dll.add(second_node)
62+ >>> dll
63+ DoubleLinkedList,
64+ Node: key: None, val: None, freq: 0, has next: True, has prev: False,
65+ Node: key: 1, val: 10, freq: 1, has next: True, has prev: True,
66+ Node: key: 2, val: 20, freq: 1, has next: True, has prev: True,
67+ Node: key: None, val: None, freq: 0, has next: False, has prev: True
68+
69+ >>> removed_node = dll.remove(first_node)
70+ >>> assert removed_node == first_node
71+ >>> dll
72+ DoubleLinkedList,
73+ Node: key: None, val: None, freq: 0, has next: True, has prev: False,
74+ Node: key: 2, val: 20, freq: 1, has next: True, has prev: True,
75+ Node: key: None, val: None, freq: 0, has next: False, has prev: True
76+
77+
78+ >>> # Attempt to remove node not on list
79+ >>> removed_node = dll.remove(first_node)
80+ >>> removed_node is None
81+ True
82+
83+ >>> # Attempt to remove head or rear
84+ >>> dll.head
85+ Node: key: None, val: None, freq: 0, has next: True, has prev: False
86+ >>> dll.remove(dll.head) is None
87+ True
88+
89+ >>> # Attempt to remove head or rear
90+ >>> dll.rear
91+ Node: key: None, val: None, freq: 0, has next: False, has prev: True
92+ >>> dll.remove(dll.rear) is None
93+ True
94+
95+
2296 """
2397
24- def __init__ (self ):
25- self .head = DoubleLinkedListNode (None , None )
26- self .rear = DoubleLinkedListNode (None , None )
98+ def __init__ (self ) -> None :
99+ self .head : DoubleLinkedListNode [ T , U ] = DoubleLinkedListNode (None , None )
100+ self .rear : DoubleLinkedListNode [ T , U ] = DoubleLinkedListNode (None , None )
27101 self .head .next , self .rear .prev = self .rear , self .head
28102
29- def add (self , node : DoubleLinkedListNode ) -> None :
103+ def __repr__ (self ) -> str :
104+ rep = ["DoubleLinkedList" ]
105+ node = self .head
106+ while node .next is not None :
107+ rep .append (str (node ))
108+ node = node .next
109+ rep .append (str (self .rear ))
110+ return ",\n " .join (rep )
111+
112+ def add (self , node : DoubleLinkedListNode [T , U ]) -> None :
30113 """
31- Adds the given node at the head of the list and shifting it to proper position
114+ Adds the given node at the tail of the list and shifting it to proper position
32115 """
33116
34- temp = self .rear .prev
117+ previous = self .rear .prev
118+
119+ # All nodes other than self.head are guaranteed to have non-None previous
120+ assert previous is not None
35121
36- self .rear .prev , node .next = node , self .rear
37- temp .next , node .prev = node , temp
122+ previous .next = node
123+ node .prev = previous
124+ self .rear .prev = node
125+ node .next = self .rear
38126 node .freq += 1
39127 self ._position_node (node )
40128
41- def _position_node (self , node : DoubleLinkedListNode ) -> None :
42- while node .prev .key and node .prev .freq > node .freq :
43- node1 , node2 = node , node .prev
44- node1 .prev , node2 .next = node2 .prev , node1 .prev
45- node1 .next , node2 .prev = node2 , node1
129+ def _position_node (self , node : DoubleLinkedListNode [T , U ]) -> None :
130+ """
131+ Moves node forward to maintain invariant of sort by freq value
132+ """
133+
134+ while node .prev is not None and node .prev .freq > node .freq :
135+ # swap node with previous node
136+ previous_node = node .prev
46137
47- def remove (self , node : DoubleLinkedListNode ) -> DoubleLinkedListNode :
138+ node .prev = previous_node .prev
139+ previous_node .next = node .prev
140+ node .next = previous_node
141+ previous_node .prev = node
142+
143+ def remove (
144+ self , node : DoubleLinkedListNode [T , U ]
145+ ) -> DoubleLinkedListNode [T , U ] | None :
48146 """
49147 Removes and returns the given node from the list
148+
149+ Returns None if node.prev or node.next is None
50150 """
51151
52- temp_last , temp_next = node .prev , node .next
53- node .prev , node .next = None , None
54- temp_last .next , temp_next .prev = temp_next , temp_last
152+ if node .prev is None or node .next is None :
153+ return None
154+
155+ node .prev .next = node .next
156+ node .next .prev = node .prev
157+ node .prev = None
158+ node .next = None
55159 return node
56160
57161
58- class LFUCache :
162+ class LFUCache ( Generic [ T , U ]) :
59163 """
60164 LFU Cache to store a given capacity of data. Can be used as a stand-alone object
61165 or as a function decorator.
@@ -66,9 +170,11 @@ class LFUCache:
66170 >>> cache.get(1)
67171 1
68172 >>> cache.set(3, 3)
69- >>> cache.get(2) # None is returned
173+ >>> cache.get(2) is None
174+ True
70175 >>> cache.set(4, 4)
71- >>> cache.get(1) # None is returned
176+ >>> cache.get(1) is None
177+ True
72178 >>> cache.get(3)
73179 3
74180 >>> cache.get(4)
@@ -89,15 +195,15 @@ class LFUCache:
89195 """
90196
91197 # class variable to map the decorator functions to their respective instance
92- decorator_function_to_instance_map = {}
198+ decorator_function_to_instance_map : dict [ Callable [[ T ], U ], LFUCache [ T , U ]] = {}
93199
94200 def __init__ (self , capacity : int ):
95- self .list = DoubleLinkedList ()
201+ self .list : DoubleLinkedList [ T , U ] = DoubleLinkedList ()
96202 self .capacity = capacity
97203 self .num_keys = 0
98204 self .hits = 0
99205 self .miss = 0
100- self .cache = {}
206+ self .cache : dict [ T , DoubleLinkedListNode [ T , U ]] = {}
101207
102208 def __repr__ (self ) -> str :
103209 """
@@ -110,73 +216,93 @@ def __repr__(self) -> str:
110216 f"capacity={ self .capacity } , current_size={ self .num_keys } )"
111217 )
112218
113- def __contains__ (self , key : int ) -> bool :
219+ def __contains__ (self , key : T ) -> bool :
114220 """
115221 >>> cache = LFUCache(1)
222+
116223 >>> 1 in cache
117224 False
225+
118226 >>> cache.set(1, 1)
119227 >>> 1 in cache
120228 True
121229 """
230+
122231 return key in self .cache
123232
124- def get (self , key : int ) -> int | None :
233+ def get (self , key : T ) -> U | None :
125234 """
126235 Returns the value for the input key and updates the Double Linked List. Returns
127- None if key is not present in cache
236+ Returns None if key is not present in cache
128237 """
129238
130239 if key in self .cache :
131240 self .hits += 1
132- self .list .add (self .list .remove (self .cache [key ]))
133- return self .cache [key ].val
241+ value_node : DoubleLinkedListNode [T , U ] = self .cache [key ]
242+ node = self .list .remove (self .cache [key ])
243+ assert node == value_node
244+
245+ # node is guaranteed not None because it is in self.cache
246+ assert node is not None
247+ self .list .add (node )
248+ return node .val
134249 self .miss += 1
135250 return None
136251
137- def set (self , key : int , value : int ) -> None :
252+ def set (self , key : T , value : U ) -> None :
138253 """
139254 Sets the value for the input key and updates the Double Linked List
140255 """
141256
142257 if key not in self .cache :
143258 if self .num_keys >= self .capacity :
144- key_to_delete = self .list .head .next .key
145- self .list .remove (self .cache [key_to_delete ])
146- del self .cache [key_to_delete ]
259+ # delete first node when over capacity
260+ first_node = self .list .head .next
261+
262+ # guaranteed to have a non-None first node when num_keys > 0
263+ # explain to type checker via assertions
264+ assert first_node is not None
265+ assert first_node .key is not None
266+ assert self .list .remove (first_node ) is not None
267+ # first_node guaranteed to be in list
268+
269+ del self .cache [first_node .key ]
147270 self .num_keys -= 1
148271 self .cache [key ] = DoubleLinkedListNode (key , value )
149272 self .list .add (self .cache [key ])
150273 self .num_keys += 1
151274
152275 else :
153276 node = self .list .remove (self .cache [key ])
277+ assert node is not None # node guaranteed to be in list
154278 node .val = value
155279 self .list .add (node )
156280
157- @staticmethod
158- def decorator (size : int = 128 ):
281+ @classmethod
282+ def decorator (
283+ cls : type [LFUCache [T , U ]], size : int = 128
284+ ) -> Callable [[Callable [[T ], U ]], Callable [..., U ]]:
159285 """
160286 Decorator version of LFU Cache
287+
288+ Decorated function must be function of T -> U
161289 """
162290
163- def cache_decorator_inner (func : Callable ) :
164- def cache_decorator_wrapper (* args , ** kwargs ) :
165- if func not in LFUCache .decorator_function_to_instance_map :
166- LFUCache .decorator_function_to_instance_map [func ] = LFUCache (size )
291+ def cache_decorator_inner (func : Callable [[ T ], U ]) -> Callable [..., U ] :
292+ def cache_decorator_wrapper (* args : T ) -> U :
293+ if func not in cls .decorator_function_to_instance_map :
294+ cls .decorator_function_to_instance_map [func ] = LFUCache (size )
167295
168- result = LFUCache .decorator_function_to_instance_map [func ].get (args [0 ])
296+ result = cls .decorator_function_to_instance_map [func ].get (args [0 ])
169297 if result is None :
170- result = func (* args , ** kwargs )
171- LFUCache .decorator_function_to_instance_map [func ].set (
172- args [0 ], result
173- )
298+ result = func (* args )
299+ cls .decorator_function_to_instance_map [func ].set (args [0 ], result )
174300 return result
175301
176- def cache_info ():
177- return LFUCache .decorator_function_to_instance_map [func ]
302+ def cache_info () -> LFUCache [ T , U ] :
303+ return cls .decorator_function_to_instance_map [func ]
178304
179- cache_decorator_wrapper . cache_info = cache_info
305+ setattr ( cache_decorator_wrapper , " cache_info" , cache_info )
180306
181307 return cache_decorator_wrapper
182308
0 commit comments