|
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