├── 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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
210 |
211 | Github repo for the complete implementation.
212 |
--------------------------------------------------------------------------------