├── lfucache ├── __init__.py ├── test │ ├── coverage.sh │ ├── all_tests.py │ └── test_lfu_cache.py └── lfu_cache.py ├── pep8.sh ├── setup.cfg ├── coverage.sh ├── MANIFEST ├── README.rst ├── setup.py ├── .gitignore ├── LICENSE └── post.txt /lfucache/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pep8.sh: -------------------------------------------------------------------------------- 1 | flake8 lfucache 2 | exit 3 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | -------------------------------------------------------------------------------- /coverage.sh: -------------------------------------------------------------------------------- 1 | coverage run lfucache/test/test_lfu_cache.py 2 | coverage report -m 3 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | # file GENERATED by distutils, do NOT edit 2 | setup.cfg 3 | setup.py 4 | lfucache/__init__.py 5 | lfucache/lfu_cache.py 6 | -------------------------------------------------------------------------------- /lfucache/test/coverage.sh: -------------------------------------------------------------------------------- 1 | coverage run --source=$PYTHONPATH/lfucache --omit=$PYTHONPATH/lfucache/test/* all_tests.py 2 | coverage report --omit=$PYTHONPATH/lfucache/test/* -m 3 | -------------------------------------------------------------------------------- /lfucache/test/all_tests.py: -------------------------------------------------------------------------------- 1 | """Run all of the tests.""" 2 | import sys 3 | import unittest2 as unittest 4 | 5 | 6 | def main(args=None): 7 | unittest_dir = '.' 8 | unittest_suite = unittest.defaultTestLoader.discover(unittest_dir) 9 | 10 | kwargs = {} 11 | if args and '-v' in args: 12 | kwargs['verbosity'] = 2 13 | runner = unittest.TextTestRunner(sys.stdout, "Unittests", 14 | **kwargs) 15 | results = runner.run(unittest_suite) 16 | return results.wasSuccessful() 17 | 18 | if __name__ == '__main__': 19 | status = main(sys.argv[1:]) 20 | sys.exit(int(not status)) 21 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | 2 | LFUCache 3 | ======== 4 | 5 | .. image:: https://img.shields.io/pypi/v/lfucache.svg 6 | :target: https://pypi.python.org/pypi/LFUCache 7 | 8 | Cache with LFU eviction scheme implemented in Python with complexity O(1) for insertion, access and deletion. 9 | 10 | .. code-block:: python 11 | 12 | >>> import lfucache.lfu_cache as lfu_cache 13 | 14 | >>> cache = lfu_cache.Cache() 15 | 16 | >>> cache.insert('k1', 'v1') 17 | >>> cache.insert('k2', 'v2') 18 | >>> cache.insert('k3', 'v3') 19 | >>> cache 20 | 1: ['k1', 'k2', 'k3'] 21 | 22 | >>> cache.access('k2') 23 | 'v2' 24 | >>> cache 25 | 1: ['k1', 'k3'] 26 | 2: ['k2'] 27 | 28 | >>> cache.get_lfu() 29 | ('k1', 'v1') 30 | 31 | >>> cache.delete_lfu() 32 | >>> cache 33 | 1: ['k3'] 34 | 2: ['k2'] 35 | 36 | 37 | More details: http://www.laurentluce.com/posts/least-frequently-used-cache-eviction-scheme-with-complexity-o1-in-python 38 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | try: 2 | from setuptools import setup 3 | except ImportError: 4 | from distutils.core import setup 5 | 6 | setup( 7 | name='LFUCache', 8 | description='Cache with LFU eviction scheme.', 9 | version='1.0.0', 10 | packages=['lfucache',], 11 | license='MIT', 12 | author='Laurent Luce', 13 | author_email='laurentluce49@yahoo.com', 14 | url='https://github.com/laurentluce/lfu-cache', 15 | keywords='lfu cache insertion access deletion', 16 | long_description='Cache with LFU eviction scheme implemented in Python with complexity O(1) for insertion, access and deletion.', 17 | classifiers=( 18 | 'Development Status :: 5 - Production/Stable', 19 | 'Intended Audience :: Developers', 20 | 'Natural Language :: English', 21 | 'License :: OSI Approved :: MIT License', 22 | 'Programming Language :: Python', 23 | 'Programming Language :: Python :: 2.7', 24 | ), 25 | ) 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Laurent Luce 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /lfucache/lfu_cache.py: -------------------------------------------------------------------------------- 1 | class Node(object): 2 | """Node containing data, pointers to previous and next node.""" 3 | 4 | def __init__(self, data): 5 | self.data = data 6 | self.prev = None 7 | self.next = None 8 | 9 | 10 | class DoublyLinkedList(object): 11 | def __init__(self): 12 | self.head = None 13 | self.tail = None 14 | # Number of nodes in list. 15 | self.count = 0 16 | 17 | def add_node(self, cls, data): 18 | """Add node instance of class cls.""" 19 | 20 | return self.insert_node(cls, data, self.tail, None) 21 | 22 | def insert_node(self, cls, data, prev, next): 23 | """Insert node instance of class cls.""" 24 | 25 | node = cls(data) 26 | node.prev = prev 27 | node.next = next 28 | if prev: 29 | prev.next = node 30 | if next: 31 | next.prev = node 32 | if not self.head or next is self.head: 33 | self.head = node 34 | if not self.tail or prev is self.tail: 35 | self.tail = node 36 | self.count += 1 37 | return node 38 | 39 | def remove_node(self, node): 40 | if node is self.tail: 41 | self.tail = node.prev 42 | else: 43 | node.next.prev = node.prev 44 | if node is self.head: 45 | self.head = node.next 46 | else: 47 | node.prev.next = node.next 48 | self.count -= 1 49 | 50 | def remove_node_by_data(self, data): 51 | """Remove node which data is equal to data.""" 52 | 53 | node = self.head 54 | while node: 55 | if node.data == data: 56 | self.remove_node(node) 57 | break 58 | node = node.next 59 | 60 | def get_nodes_data(self): 61 | """Return list nodes data as a list.""" 62 | 63 | data = [] 64 | node = self.head 65 | while node: 66 | data.append(node.data) 67 | node = node.next 68 | return data 69 | 70 | 71 | class FreqNode(DoublyLinkedList, Node): 72 | """Frequency node. 73 | 74 | Frequency node contains a linked list of item nodes with same frequency. 75 | """ 76 | 77 | def __init__(self, data): 78 | DoublyLinkedList.__init__(self) 79 | Node.__init__(self, data) 80 | 81 | def add_item_node(self, data): 82 | node = self.add_node(ItemNode, data) 83 | node.parent = self 84 | return node 85 | 86 | def insert_item_node(self, data, prev, next): 87 | node = self.insert_node(ItemNode, data, prev, next) 88 | node.parent = self 89 | return node 90 | 91 | def remove_item_node(self, node): 92 | self.remove_node(node) 93 | 94 | def remove_item_node_by_data(self, data): 95 | self.remove_node_by_data(data) 96 | 97 | 98 | class ItemNode(Node): 99 | def __init__(self, data): 100 | Node.__init__(self, data) 101 | self.parent = None 102 | 103 | 104 | class LfuItem(object): 105 | def __init__(self, data, parent, node): 106 | self.data = data 107 | self.parent = parent 108 | self.node = node 109 | 110 | 111 | class Cache(DoublyLinkedList): 112 | def __init__(self): 113 | DoublyLinkedList.__init__(self) 114 | self.items = dict() 115 | 116 | def insert_freq_node(self, data, prev, next): 117 | return self.insert_node(FreqNode, data, prev, next) 118 | 119 | def remove_freq_node(self, node): 120 | self.remove_node(node) 121 | 122 | def access(self, key): 123 | try: 124 | tmp = self.items[key] 125 | except KeyError: 126 | raise NotFoundException('Key not found') 127 | 128 | freq_node = tmp.parent 129 | next_freq_node = freq_node.next 130 | 131 | if not next_freq_node or next_freq_node.data != freq_node.data + 1: 132 | next_freq_node = self.insert_freq_node(freq_node.data + 1, 133 | freq_node, next_freq_node) 134 | item_node = next_freq_node.add_item_node(key) 135 | tmp.parent = next_freq_node 136 | 137 | freq_node.remove_item_node(tmp.node) 138 | if freq_node.count == 0: 139 | self.remove_freq_node(freq_node) 140 | 141 | tmp.node = item_node 142 | return tmp.data 143 | 144 | def insert(self, key, value): 145 | if key in self.items: 146 | raise DuplicateException('Key exists') 147 | freq_node = self.head 148 | if not freq_node or freq_node.data != 1: 149 | freq_node = self.insert_freq_node(1, None, freq_node) 150 | 151 | item_node = freq_node.add_item_node(key) 152 | self.items[key] = LfuItem(value, freq_node, item_node) 153 | 154 | def get_lfu(self): 155 | if not len(self.items): 156 | raise NotFoundException('Items list is empty.') 157 | return self.head.head.data, self.items[self.head.head.data].data 158 | 159 | def delete_lfu(self): 160 | """Remove the first item node from the first frequency node. 161 | 162 | Remove the LFU item from the dictionary. 163 | """ 164 | if not self.head: 165 | raise NotFoundException('No frequency nodes found') 166 | freq_node = self.head 167 | item_node = freq_node.head 168 | del self.items[item_node.data] 169 | freq_node.remove_item_node(item_node) 170 | if freq_node.count == 0: 171 | self.remove_freq_node(freq_node) 172 | 173 | def __repr__(self): 174 | """Display access frequency list and items. 175 | 176 | Using the representation: 177 | freq1: [item, item, ...] 178 | freq2: [item, item] 179 | ... 180 | """ 181 | s = '' 182 | freq_node = self.head 183 | while freq_node: 184 | s += '%s: %s\n' % (freq_node.data, freq_node.get_nodes_data()) 185 | freq_node = freq_node.next 186 | return s 187 | 188 | 189 | class DuplicateException(Exception): 190 | pass 191 | 192 | 193 | class NotFoundException(Exception): 194 | pass 195 | -------------------------------------------------------------------------------- /lfucache/test/test_lfu_cache.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import lfucache.lfu_cache as lfu_cache 4 | 5 | 6 | class TestLfuCache(unittest.TestCase): 7 | 8 | def setUp(self): 9 | self.cache = lfu_cache.Cache() 10 | 11 | def test_insert_freq_nodes(self): 12 | node2 = self.cache.insert_freq_node(2, None, None) 13 | node1 = self.cache.insert_freq_node(1, None, node2) 14 | node4 = self.cache.insert_freq_node(4, node2, None) 15 | node3 = self.cache.insert_freq_node(3, node2, node4) 16 | 17 | nodes_data = self.cache.get_nodes_data() 18 | self.assertEqual(nodes_data, [1, 2, 3, 4]) 19 | self.assertEqual(self.cache.count, 4) 20 | 21 | self.cache.remove_freq_node(node1) 22 | nodes_data = self.cache.get_nodes_data() 23 | self.assertEqual(nodes_data, [2, 3, 4]) 24 | self.assertEqual(self.cache.count, 3) 25 | 26 | self.cache.remove_freq_node(node3) 27 | nodes_data = self.cache.get_nodes_data() 28 | self.assertEqual(nodes_data, [2, 4]) 29 | 30 | def test_add_freq_item_nodes(self): 31 | freq_node1 = self.cache.insert_freq_node(1, None, None) 32 | freq_node2 = self.cache.insert_freq_node(2, freq_node1, None) 33 | freq_node1_item_node1 = freq_node1.insert_item_node('a', None, None) 34 | freq_node1.insert_item_node('b', freq_node1_item_node1, None) 35 | freq_node2_item_node2 = freq_node2.insert_item_node('d', None, None) 36 | freq_node2_item_node1 = freq_node2.insert_item_node( 37 | 'c', None, 38 | freq_node2_item_node2) 39 | freq_node2_item_node4 = freq_node2.insert_item_node( 40 | 'f', freq_node2_item_node2, None) 41 | freq_node2_item_node3 = freq_node2.insert_item_node( 42 | 'e', freq_node2_item_node2, freq_node2_item_node4) 43 | freq_node2.add_item_node('g') 44 | 45 | nodes_data = freq_node1.get_nodes_data() 46 | self.assertEqual(nodes_data, ['a', 'b']) 47 | 48 | nodes_data = freq_node2.get_nodes_data() 49 | self.assertEqual(nodes_data, ['c', 'd', 'e', 'f', 'g']) 50 | 51 | freq_node2.remove_item_node(freq_node2_item_node1) 52 | nodes_data = freq_node2.get_nodes_data() 53 | self.assertEqual(nodes_data, ['d', 'e', 'f', 'g']) 54 | 55 | freq_node2.remove_item_node(freq_node2_item_node3) 56 | nodes_data = freq_node2.get_nodes_data() 57 | self.assertEqual(nodes_data, ['d', 'f', 'g']) 58 | 59 | freq_node2.remove_item_node_by_data('f') 60 | nodes_data = freq_node2.get_nodes_data() 61 | self.assertEqual(nodes_data, ['d', 'g']) 62 | 63 | def test_insert(self): 64 | self.cache.insert('k1', 'd1') 65 | self.cache.insert('k2', 'd2') 66 | self.cache.insert('k3', 'd3') 67 | 68 | nodes_data = self.cache.get_nodes_data() 69 | self.assertEqual(nodes_data, [1, ]) 70 | 71 | nodes_data = self.cache.head.get_nodes_data() 72 | self.assertEqual(nodes_data, ['k1', 'k2', 'k3']) 73 | 74 | self.assertEqual(self.cache.items['k1'].data, 'd1') 75 | self.assertEqual(self.cache.items['k1'].parent, self.cache.head) 76 | self.assertEqual(self.cache.items['k1'].node, self.cache.head.head) 77 | self.assertEqual(self.cache.items['k2'].data, 'd2') 78 | self.assertEqual(self.cache.items['k2'].parent, self.cache.head) 79 | self.assertEqual(self.cache.items['k2'].node, 80 | self.cache.head.head.next) 81 | self.assertEqual(self.cache.items['k3'].data, 'd3') 82 | self.assertEqual(self.cache.items['k3'].parent, self.cache.head) 83 | self.assertEqual(self.cache.items['k3'].node, 84 | self.cache.head.head.next.next) 85 | 86 | self.assertRaises(lfu_cache.DuplicateException, self.cache.insert, 87 | 'k3', 'd3') 88 | 89 | def test_access(self): 90 | self.cache.insert('k1', 'd1') 91 | self.cache.insert('k2', 'd2') 92 | self.cache.insert('k3', 'd3') 93 | 94 | self.assertEqual(self.cache.access('k2'), 'd2') 95 | self.assertEqual(self.cache.access('k3'), 'd3') 96 | self.assertEqual(self.cache.access('k3'), 'd3') 97 | 98 | nodes_data = self.cache.get_nodes_data() 99 | self.assertEqual(nodes_data, [1, 2, 3]) 100 | 101 | nodes_data = self.cache.head.get_nodes_data() 102 | self.assertEqual(nodes_data, ['k1']) 103 | nodes_data = self.cache.head.next.get_nodes_data() 104 | self.assertEqual(nodes_data, ['k2']) 105 | nodes_data = self.cache.head.next.next.get_nodes_data() 106 | self.assertEqual(nodes_data, ['k3']) 107 | 108 | self.assertEqual(str(self.cache), "1: ['k1']\n2: ['k2']\n3: ['k3']\n") 109 | 110 | self.assertEqual(self.cache.access('k1'), 'd1') 111 | self.assertEqual(self.cache.access('k3'), 'd3') 112 | 113 | nodes_data = self.cache.get_nodes_data() 114 | self.assertEqual(nodes_data, [2, 4]) 115 | 116 | nodes_data = self.cache.head.get_nodes_data() 117 | self.assertEqual(nodes_data, ['k2', 'k1']) 118 | nodes_data = self.cache.head.next.get_nodes_data() 119 | self.assertEqual(nodes_data, ['k3']) 120 | 121 | self.assertRaises(lfu_cache.NotFoundException, self.cache.access, 'k4') 122 | 123 | def test_get_lfu(self): 124 | self.assertRaises(lfu_cache.NotFoundException, 125 | self.cache.get_lfu) 126 | 127 | self.cache.insert('k1', 'd1') 128 | self.cache.insert('k2', 'd2') 129 | self.cache.insert('k3', 'd3') 130 | 131 | self.assertEqual(self.cache.access('k2'), 'd2') 132 | self.assertEqual(self.cache.access('k3'), 'd3') 133 | self.assertEqual(self.cache.access('k3'), 'd3') 134 | 135 | self.assertEqual(self.cache.get_lfu(), ('k1', 'd1')) 136 | 137 | self.assertEqual(self.cache.access('k1'), 'd1') 138 | 139 | self.assertEqual(self.cache.get_lfu(), ('k2', 'd2')) 140 | 141 | def test_delete_lfu(self): 142 | self.assertRaises(lfu_cache.NotFoundException, 143 | self.cache.delete_lfu) 144 | 145 | self.cache.insert('k1', 'd1') 146 | self.cache.insert('k2', 'd2') 147 | self.cache.insert('k3', 'd3') 148 | self.assertEqual(self.cache.access('k2'), 'd2') 149 | self.assertEqual(self.cache.get_lfu(), ('k1', 'd1')) 150 | 151 | self.cache.delete_lfu() 152 | self.assertEqual(self.cache.get_lfu(), ('k3', 'd3')) 153 | 154 | self.cache.delete_lfu() 155 | self.assertEqual(self.cache.get_lfu(), ('k2', 'd2')) 156 | 157 | 158 | if __name__ == '__main__': 159 | unittest.main() 160 | -------------------------------------------------------------------------------- /post.txt: -------------------------------------------------------------------------------- 1 | This post describes the implementation in Python of a "Least Frequently Used" (LFU) algorithm cache eviction scheme with complexity O(1). The algorithm is described in this paper written by Prof. Ketan Shah, Anirban Mitra and Dhruv Matani. The naming in the implementation follows the naming in the paper. 2 | 3 | LFU cache eviction scheme is useful for an HTTP caching network proxy for example, where we want the least frequently used items to be removed from the cache. 4 | 5 | The goal here is for the LFU cache algorithm to have a runtime complexity of O(1) for all of its operations, which include insertion, access and deletion (eviction). 6 | 7 | Doubly linked lists are used in this algorithm. One for the access frequency and each node in that list contains a list with the elements of same access frequency. Let say we have five elements in our cache. Two have been accessed one time and two have been accessed two times. In that case, the access frequency list has two nodes (frequency = 1 and frequency = 2). The first frequency node has two nodes in its list and the second frequency node has three nodes in its list. 8 | 9 | LFU doubly linked lists. 10 | 11 | How do we build that? The first object we need is a node: 12 | 13 | [code lang="python"] 14 | class Node(object): 15 | """Node containing data, pointers to previous and next node.""" 16 | def __init__(self, data): 17 | self.data = data 18 | self.prev = None 19 | self.next = None 20 | [/code] 21 | 22 | Next, our doubly linked list. Each node has a prev and next attribute equal to the previous node and next node respectively. The head is set to the first node and the tail to the last node. 23 | 24 | LFU doubly linked list. 25 | 26 | We can define our doubly linked list with methods to add a node at the end of the list, insert a node, remove a node and get a list with the nodes data. 27 | 28 | [code lang="python"] 29 | class DoublyLinkedList(object): 30 | def __init__(self): 31 | self.head = None 32 | self.tail = None 33 | # Number of nodes in list. 34 | self.count = 0 35 | 36 | def add_node(self, cls, data): 37 | """Add node instance of class cls.""" 38 | return self.insert_node(cls, data, self.tail, None) 39 | 40 | def insert_node(self, cls, data, prev, next): 41 | """Insert node instance of class cls.""" 42 | node = cls(data) 43 | node.prev = prev 44 | node.next = next 45 | if prev: 46 | prev.next = node 47 | if next: 48 | next.prev = node 49 | if not self.head or next is self.head: 50 | self.head = node 51 | if not self.tail or prev is self.tail: 52 | self.tail = node 53 | self.count += 1 54 | return node 55 | 56 | def remove_node(self, node): 57 | if node is self.tail: 58 | self.tail = node.prev 59 | else: 60 | node.next.prev = node.prev 61 | if node is self.head: 62 | self.head = node.next 63 | else: 64 | node.prev.next = node.next 65 | self.count -= 1 66 | 67 | def get_nodes_data(self): 68 | """Return list nodes data as a list.""" 69 | data = [] 70 | node = self.head 71 | while node: 72 | data.append(node.data) 73 | node = node.next 74 | return data 75 | [/code] 76 | 77 | Each node in the access frequency doubly linked list is a frequency node (Freq Node on the diagram below). It is a node and also a doubly linked list containing the elements (Item nodes on the diagram below) of same frequency. Each item node has a pointer to its frequency node parent. 78 | 79 | LFU frequency and items doubly linked list. 80 | 81 | [code lang="python"] 82 | class FreqNode(DoublyLinkedList, Node): 83 | """Frequency node containing linked list of item nodes with 84 | same frequency.""" 85 | def __init__(self, data): 86 | DoublyLinkedList.__init__(self) 87 | Node.__init__(self, data) 88 | 89 | def add_item_node(self, data): 90 | node = self.add_node(ItemNode, data) 91 | node.parent = self 92 | return node 93 | 94 | def insert_item_node(self, data, prev, next): 95 | node = self.insert_node(ItemNode, data, prev, next) 96 | node.parent = self 97 | return node 98 | 99 | def remove_item_node(self, node): 100 | self.remove_node(node) 101 | 102 | 103 | class ItemNode(Node): 104 | def __init__(self, data): 105 | Node.__init__(self, data) 106 | self.parent = None 107 | [/code] 108 | 109 | The item node data is equal to the key of the element we are storing, an HTTP request could be the key. The content itself (HTTP response for example) is stored in a dictionary. Each value in this dictionary is of type LfuItem where "data" is the content cached, "parent" is a pointer to the frequency node and "node" is a pointer to the item node under the frequency node. 110 | 111 | LFU Item. 112 | 113 | [code lang="python"] 114 | class LfuItem(object): 115 | def __init__(self, data, parent, node): 116 | self.data = data 117 | self.parent = parent 118 | self.node = node 119 | [/code] 120 | 121 | We have defined our data objects classes, now we can define our cache object class. It has a doubly linked list (access frequency list) and a dictionary to contain the LFU items (LfuItem above). We defined two methods: one to insert a frequency node and one to remove a frequency node. 122 | 123 | [code lang="python"] 124 | class Cache(DoublyLinkedList): 125 | def __init__(self): 126 | DoublyLinkedList.__init__(self) 127 | self.items = dict() 128 | 129 | def insert_freq_node(self, data, prev, next): 130 | return self.insert_node(FreqNode, data, prev, next) 131 | 132 | def remove_freq_node(self, node): 133 | self.remove_node(node) 134 | [/code] 135 | 136 | Next step is to define methods to insert to the cache, access the cache and delete from the cache. 137 | 138 | Let's look at the insert method logic. It takes a key and value, for example HTTP request and response. If the frequency node with frequency one does not exist, it is inserted at the beginning of the access frequency linked list. An item node is added to the frequency node items linked list. The key and value are added to the dictionary. 139 | 140 | [code lang="python"] 141 | def insert(self, key, value): 142 | if key in self.items: 143 | raise DuplicateException('Key exists') 144 | freq_node = self.head 145 | if not freq_node or freq_node.data != 1: 146 | freq_node = self.insert_freq_node(1, None, freq_node) 147 | 148 | freq_node.add_item_node(key) 149 | self.items[key] = LfuItem(value, freq_node) 150 | [/code] 151 | 152 | We insert two elements in our cache, we end up with: 153 | 154 | LFU insert method. 155 | 156 | Let's look at the access method logic. If the key does not exist, we raise an exception. If the key exists, we move the item node to the frequency node list with frequency + 1 (adding the frequency node if it does not exist). 157 | 158 | [code lang="python"] 159 | def access(self, key): 160 | try: 161 | tmp = self.items[key] 162 | except KeyError: 163 | raise NotFoundException('Key not found') 164 | 165 | freq_node = tmp.parent 166 | next_freq_node = freq_node.next 167 | 168 | if not next_freq_node or next_freq_node.data != freq_node.data + 1: 169 | next_freq_node = self.insert_freq_node(freq_node.data + 1, 170 | freq_node, next_freq_node) 171 | item_node = next_freq_node.add_item_node(key) 172 | tmp.parent = next_freq_node 173 | 174 | freq_node.remove_item_node(tmp.node) 175 | if freq_node.count == 0: 176 | self.remove_freq_node(freq_node) 177 | 178 | tmp.node = item_node 179 | return tmp.data 180 | [/code] 181 | 182 | If we access the item with Key 1, the item node with data Key 1 is moved to the frequency node with frequency equal to 2. We end up with: 183 | 184 | LFU access method. 185 | 186 | If we access the item with Key 2, the item node with data Key 2 is moved to the frequency node with frequency equal to 2. The frequency node 1 is removed. We end up with: 187 | 188 | LFU access 2 method. 189 | 190 | Let's look at the delete_lfu method. It removes the least frequently used item from the cache. To do that, it removes the first item node from the first frequency node and also the LFUItem object from the dictionary. If after this operation, the frequency node list is empty, it is removed. 191 | 192 | [code lang="python"] 193 | def delete_lfu(self): 194 | """Remove the first item node from the first frequency node. 195 | Remove the item from the dictionary. 196 | """ 197 | if not self.head: 198 | raise NotFoundException('No frequency nodes found') 199 | freq_node = self.head 200 | item_node = freq_node.head 201 | del self.items[item_node.data] 202 | freq_node.remove_item_node(item_node) 203 | if freq_node.count == 0: 204 | self.remove_freq_node(freq_node) 205 | [/code] 206 | 207 | If we call delete_lfu on our cache, the item node with data equal to Key 1 is removed and its LFUItem too. We end up with: 208 | 209 | LFU delete method. 210 | 211 | Github repo for the complete implementation. 212 | --------------------------------------------------------------------------------