|
1 |
| -""" |
2 |
| -A fork of Python 3.6's stdlib lru_cache (found in Python's 'cpython/Lib/functools.py') |
3 |
| -adapted into a data structure for single threaded uses. |
| 1 | +from typing import TYPE_CHECKING |
4 | 2 |
|
5 |
| -https://github.com/python/cpython/blob/v3.6.12/Lib/functools.py |
| 3 | +if TYPE_CHECKING: |
| 4 | + from typing import Any |
6 | 5 |
|
7 | 6 |
|
8 |
| -Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, |
9 |
| -2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Python Software Foundation; |
10 |
| -
|
11 |
| -All Rights Reserved |
12 |
| -
|
13 |
| -
|
14 |
| -PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 |
15 |
| --------------------------------------------- |
16 |
| -
|
17 |
| -1. This LICENSE AGREEMENT is between the Python Software Foundation |
18 |
| -("PSF"), and the Individual or Organization ("Licensee") accessing and |
19 |
| -otherwise using this software ("Python") in source or binary form and |
20 |
| -its associated documentation. |
21 |
| -
|
22 |
| -2. Subject to the terms and conditions of this License Agreement, PSF hereby |
23 |
| -grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, |
24 |
| -analyze, test, perform and/or display publicly, prepare derivative works, |
25 |
| -distribute, and otherwise use Python alone or in any derivative version, |
26 |
| -provided, however, that PSF's License Agreement and PSF's notice of copyright, |
27 |
| -i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, |
28 |
| -2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Python Software Foundation; |
29 |
| -All Rights Reserved" are retained in Python alone or in any derivative version |
30 |
| -prepared by Licensee. |
31 |
| -
|
32 |
| -3. In the event Licensee prepares a derivative work that is based on |
33 |
| -or incorporates Python or any part thereof, and wants to make |
34 |
| -the derivative work available to others as provided herein, then |
35 |
| -Licensee hereby agrees to include in any such work a brief summary of |
36 |
| -the changes made to Python. |
37 |
| -
|
38 |
| -4. PSF is making Python available to Licensee on an "AS IS" |
39 |
| -basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR |
40 |
| -IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND |
41 |
| -DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS |
42 |
| -FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT |
43 |
| -INFRINGE ANY THIRD PARTY RIGHTS. |
44 |
| -
|
45 |
| -5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON |
46 |
| -FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS |
47 |
| -A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, |
48 |
| -OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. |
49 |
| -
|
50 |
| -6. This License Agreement will automatically terminate upon a material |
51 |
| -breach of its terms and conditions. |
52 |
| -
|
53 |
| -7. Nothing in this License Agreement shall be deemed to create any |
54 |
| -relationship of agency, partnership, or joint venture between PSF and |
55 |
| -Licensee. This License Agreement does not grant permission to use PSF |
56 |
| -trademarks or trade name in a trademark sense to endorse or promote |
57 |
| -products or services of Licensee, or any third party. |
58 |
| -
|
59 |
| -8. By copying, installing or otherwise using Python, Licensee |
60 |
| -agrees to be bound by the terms and conditions of this License |
61 |
| -Agreement. |
62 |
| -
|
63 |
| -""" |
64 |
| - |
65 |
| -from copy import copy, deepcopy |
66 |
| - |
67 |
| -SENTINEL = object() |
68 |
| - |
69 |
| - |
70 |
| -# aliases to the entries in a node |
71 |
| -PREV = 0 |
72 |
| -NEXT = 1 |
73 |
| -KEY = 2 |
74 |
| -VALUE = 3 |
| 7 | +_SENTINEL = object() |
75 | 8 |
|
76 | 9 |
|
77 | 10 | class LRUCache:
|
78 | 11 | def __init__(self, max_size):
|
79 |
| - assert max_size > 0 |
80 |
| - |
| 12 | + # type: (int) -> None |
| 13 | + if max_size <= 0: |
| 14 | + raise AssertionError(f"invalid max_size: {max_size}") |
81 | 15 | self.max_size = max_size
|
82 |
| - self.full = False |
83 |
| - |
84 |
| - self.cache = {} |
85 |
| - |
86 |
| - # root of the circularly linked list to keep track of |
87 |
| - # the least recently used key |
88 |
| - self.root = [] # type: ignore |
89 |
| - # the node looks like [PREV, NEXT, KEY, VALUE] |
90 |
| - self.root[:] = [self.root, self.root, None, None] |
91 |
| - |
| 16 | + self._data = {} # type: dict[Any, Any] |
92 | 17 | self.hits = self.misses = 0
|
| 18 | + self.full = False |
93 | 19 |
|
94 | 20 | def __copy__(self):
|
95 |
| - cache = LRUCache(self.max_size) |
96 |
| - cache.full = self.full |
97 |
| - cache.cache = copy(self.cache) |
98 |
| - cache.root = deepcopy(self.root) |
99 |
| - return cache |
| 21 | + # type: () -> LRUCache |
| 22 | + new = LRUCache(max_size=self.max_size) |
| 23 | + new.hits = self.hits |
| 24 | + new.misses = self.misses |
| 25 | + new.full = self.full |
| 26 | + new._data = self._data.copy() |
| 27 | + return new |
100 | 28 |
|
101 | 29 | def set(self, key, value):
|
102 |
| - link = self.cache.get(key, SENTINEL) |
103 |
| - |
104 |
| - if link is not SENTINEL: |
105 |
| - # have to move the node to the front of the linked list |
106 |
| - link_prev, link_next, _key, _value = link |
107 |
| - |
108 |
| - # first remove the node from the lsnked list |
109 |
| - link_prev[NEXT] = link_next |
110 |
| - link_next[PREV] = link_prev |
111 |
| - |
112 |
| - # insert the node between the root and the last |
113 |
| - last = self.root[PREV] |
114 |
| - last[NEXT] = self.root[PREV] = link |
115 |
| - link[PREV] = last |
116 |
| - link[NEXT] = self.root |
117 |
| - |
118 |
| - # update the value |
119 |
| - link[VALUE] = value |
120 |
| - |
| 30 | + # type: (Any, Any) -> None |
| 31 | + current = self._data.pop(key, _SENTINEL) |
| 32 | + if current is not _SENTINEL: |
| 33 | + self._data[key] = value |
121 | 34 | elif self.full:
|
122 |
| - # reuse the root node, so update its key/value |
123 |
| - old_root = self.root |
124 |
| - old_root[KEY] = key |
125 |
| - old_root[VALUE] = value |
126 |
| - |
127 |
| - self.root = old_root[NEXT] |
128 |
| - old_key = self.root[KEY] |
129 |
| - |
130 |
| - self.root[KEY] = self.root[VALUE] = None |
131 |
| - |
132 |
| - del self.cache[old_key] |
133 |
| - |
134 |
| - self.cache[key] = old_root |
135 |
| - |
| 35 | + self._data.pop(next(iter(self._data))) |
| 36 | + self._data[key] = value |
136 | 37 | else:
|
137 |
| - # insert new node after last |
138 |
| - last = self.root[PREV] |
139 |
| - link = [last, self.root, key, value] |
140 |
| - last[NEXT] = self.root[PREV] = self.cache[key] = link |
141 |
| - self.full = len(self.cache) >= self.max_size |
| 38 | + self._data[key] = value |
| 39 | + self.full = len(self._data) >= self.max_size |
142 | 40 |
|
143 | 41 | def get(self, key, default=None):
|
144 |
| - link = self.cache.get(key, SENTINEL) |
145 |
| - |
146 |
| - if link is SENTINEL: |
| 42 | + # type: (Any, Any) -> Any |
| 43 | + try: |
| 44 | + ret = self._data.pop(key) |
| 45 | + except KeyError: |
147 | 46 | self.misses += 1
|
148 |
| - return default |
149 |
| - |
150 |
| - # have to move the node to the front of the linked list |
151 |
| - link_prev, link_next, _key, _value = link |
152 |
| - |
153 |
| - # first remove the node from the lsnked list |
154 |
| - link_prev[NEXT] = link_next |
155 |
| - link_next[PREV] = link_prev |
156 |
| - |
157 |
| - # insert the node between the root and the last |
158 |
| - last = self.root[PREV] |
159 |
| - last[NEXT] = self.root[PREV] = link |
160 |
| - link[PREV] = last |
161 |
| - link[NEXT] = self.root |
162 |
| - |
163 |
| - self.hits += 1 |
| 47 | + ret = default |
| 48 | + else: |
| 49 | + self.hits += 1 |
| 50 | + self._data[key] = ret |
164 | 51 |
|
165 |
| - return link[VALUE] |
| 52 | + return ret |
166 | 53 |
|
167 | 54 | def get_all(self):
|
168 |
| - nodes = [] |
169 |
| - node = self.root[NEXT] |
170 |
| - |
171 |
| - # To ensure the loop always terminates we iterate to the maximum |
172 |
| - # size of the LRU cache. |
173 |
| - for _ in range(self.max_size): |
174 |
| - # The cache may not be full. We exit early if we've wrapped |
175 |
| - # around to the head. |
176 |
| - if node is self.root: |
177 |
| - break |
178 |
| - nodes.append((node[KEY], node[VALUE])) |
179 |
| - node = node[NEXT] |
180 |
| - |
181 |
| - return nodes |
| 55 | + # type: () -> list[tuple[Any, Any]] |
| 56 | + return list(self._data.items()) |
0 commit comments