GeezerVisonThe World Wide Web, restored to 800 pixels of glory. |
| Home|About|Gaming cabinet|Archives|How it works|Starter sites |
GeezerVison is showing github.com — unpythonic/unpythonic/gmemo.py at master · Technologicat/unpythonic Expand file tree/ Copy pathgmemo.pyMore file actions 240 lines (194 loc) · 8.88 KB / Copy pathgmemo.pyFile metadata and controls240 lines (194 loc) · 8.88 KB 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 # -*- coding: utf-8 -*- """Memoization of generators. Memoize generator functions (generator definitions), iterators, or iterators created by factory functions.""" __all__ = ["gmemoize", "imemoize", "fimemoize"] from collections.abc import Callable, Generator, Iterable, Iterator from functools import wraps from threading import RLock from typing import Any, TypeVar from .arity import resolve_bindings, tuplify_bindings from .regutil import register_decorator from .symbol import sym F = TypeVar('F', bound=Callable) def gmemoize(gfunc: F) -> F: """Decorator: produce memoized generator instances. Similar to ``itertools.tee``, but the whole sequence is kept in memory indefinitely, so more instances of the memoized generator can be created at any time (instead of having to specify how many copies at ``tee`` time). Decorate the **generator function** (i.e. generator definition) with this. - If the gfunc takes arguments, they must be hashable. A separate memoized sequence is created for each unique set of argument values seen. - For simplicity, the generator itself may use ``yield`` for output only; ``send`` is not supported. - Any exceptions raised by the generator (except StopIteration) are also memoized, like in ``memoize``. - Thread-safe. Calls to ``next`` on the memoized generator from different threads are serialized via a lock. Each memoized sequence has its own lock. This uses ``threading.RLock``, so re-entering from the same thread (e.g. in recursively defined sequences) is fine. - For infinite sequences, use this only if you can guarantee only a reasonable number of terms will ever be evaluated (w.r.t. available RAM). - Typically, this should be the outermost decorator if several are used on the same gfunc. Usage:: evals = 0 @gmemoize def one_two_three(): global evals for j in range(3): evals += 1 yield j g1 = one_two_three() assert tuple(x for x in g1) == (0, 1, 2) g2 = one_two_three() assert tuple(x for x in g2) == (0, 1, 2) assert evals == 3 It doesn't matter when the generator instances are created, or when which of them is advanced. All instances created with the same arguments (here none) to the gfunc share the same memoized sequence. **Recipe**: *Memoizing only a part of a sequence*. Build a chain of generators, then memoize only the last one:: def orig(): yield from range(100) def evens(): yield from (x for x in orig() if x % 2 == 0) evaluations = collections.Counter() @gmemoize def some_evens(n): # drop n first terms evaluations[n] += 1 yield from drop(n, evens()) last(some_evens(25)) last(some_evens(25)) last(some_evens(20)) assert all(v == 1 for k, v in evaluations.items()) Or with ``lambda`` for a more compact presentation:: orig = lambda: (yield from range(100)) evens = lambda: (yield from (x for x in orig() if x % 2 == 0)) some_evens = gmemoize(lambda n: (yield from drop(n, evens()))) last(some_evens(25)) last(some_evens(25)) See also ``imemoize``, ``fimemoize``. """ memos: dict[tuple, tuple] = {} @wraps(gfunc) def gmemoized(*args: Any, **kwargs: Any) -> "_MemoizedGenerator": k = tuplify_bindings(resolve_bindings(gfunc, *args, **kwargs)) if k not in memos: # underlying generator instance, memo instance, lock instance memos[k] = (gfunc(*args, **kwargs), [], RLock()) return _MemoizedGenerator(*memos[k]) return gmemoized _success = sym("_success") _fail = sym("_fail") class _MemoizedGenerator: """Wrapper that manages one memoized sequence. Co-operates with gmemoize.""" def __init__(self, g: Generator, memo: list, lock: RLock) -> None: self.g = g self.memo = memo # each instance for the same g gets the same memo self.lock = lock self.j: int = 0 # current position in memo def __repr__(self) -> str: return f"<_MemoizedGenerator object {self.g.__name__} at 0x{id(self):x}>" # Support the `collections.abc.Iterable` API def __iter__(self) -> "_MemoizedGenerator": return self def __next__(self) -> Any: j = self.j memo = self.memo with self.lock: if j < len(memo): result = memo[j] else: try: result = (_success, next(self.g)) except BaseException as err: # StopIteration propagates, not a BaseException result = (_fail, err) memo.append(result) kind, value = result self.j += 1 if kind is _fail: raise value return value # Support a subset of the `collections.abc.Sequence` API for already-computed items def __len__(self) -> int: return len(self.memo) def __getitem__(self, k: int | slice) -> Any: if not isinstance(k, (int, slice)): raise TypeError(f"Expected an int or slice index, got {type(k)} with value {repr(k)}") length = len(self.memo) if isinstance(k, slice): # For slices where at least one item raises an exception, we raise the # exception that is encountered first when walking the slice. lst = [] for kind, value in self.memo[k]: if kind is _fail: raise value lst.append(value) return lst if k >= length or k < -length: raise IndexError(f"memoized generator index out of range; got {k}, with {len(self.memo)} items currently available") kind, value = self.memo[k] if kind is _fail: raise value return value def imemoize(iterable: Iterable) -> Callable: """Memoize an iterable. Return a gfunc with no parameters which, when called, returns a generator that yields items from the memoized iterable. The original iterable is used to retrieve more terms when needed. ``imemoize`` essentially makes the iterable restartable (arbitrarily "tee-able"), at the cost of keeping the whole history in memory. Unlike ``itertools.tee``, there is no need to specify how many copies are needed at ``tee`` time; a new copy can be created at any time, by calling the returned gfunc. Like ``itertools.tee``, after memoizing, the original iterator should not be used. The danger is that if something outside the memoization mechanism advances it, some values will be lost before they reach the memo. Example:: evens = (x for x in range(100) if x % 2 == 0) some_evens = imemoize(drop(25, evens)) assert last(some_evens()) == last(some_evens()) # iterating twice! In the example, whenever ``some_evens`` is called, it returns a new memoized generator instance that yields items from the memoized iterable. If you need to take arguments to create the iterable, see ``fimemoize``. """ @gmemoize def iterable_as_gfunc() -> Iterator: yield from iterable return iterable_as_gfunc @register_decorator(priority=10) def fimemoize(ifactory: F) -> F: """Like imemoize, but for cases where creating the iterable needs arguments. ``ifactory`` is a function, which takes any number of positional or keyword arguments, and returns an iterable. This is similar to ``gmemoize``, but for a regular function that returns an iterable (instead of for a gfunc). This works by defining a gfunc based on ``ifactory``, and gmemoizing that. Hence arguments of ``ifactory`` must be hashable, and each unique set of argument values produces its own memoized sequence. The return value is a gfunc, which takes the same arguments as ``ifactory``. Example:: def evens(): yield from (x for x in range(100) if x % 2 == 0) @fimemoize def some_evens(n): # regular function! return drop(n, evens()) assert last(some_evens(25)) == last(some_evens(25)) Compare to:: def evens(): yield from (x for x in range(100) if x % 2 == 0) @gmemoize def some_evens(n): # gfunc! yield from drop(n, evens()) assert last(some_evens(25)) == last(some_evens(25)) Using lambda, the original example is equivalent to:: evens = lambda: (yield from (x for x in range(100) if x % 2 == 0)) some_evens = fimemoize(lambda n: drop(n, evens())) assert last(some_evens(25)) == last(some_evens(25)) """ @wraps(ifactory) def gfunc(*args: Any, **kwargs: Any) -> Iterator: yield from ifactory(*args, **kwargs) return gmemoize(gfunc) # return gmemoize(lambda *a, **kw: (yield from ifactory(*a, **kw))) |
| You are visitor 01402520 — GeezerVison does not store the pages you look up. Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required. |