|
11 | 11 | import logging |
12 | 12 | import warnings |
13 | 13 | import enum |
| 14 | +import functools |
14 | 15 |
|
15 | 16 | from collections import defaultdict |
16 | 17 | from operator import attrgetter |
|
19 | 20 |
|
20 | 21 | import typing |
21 | 22 | from typing import ( |
22 | | - Any, Optional, List, NamedTuple, Set, Dict, |
| 23 | + Any, Optional, List, NamedTuple, Set, Dict, Callable, |
23 | 24 | Sequence, Union, DefaultDict, Type |
24 | 25 | ) |
25 | 26 |
|
@@ -95,6 +96,138 @@ class Close(Signal): ... |
95 | 96 | is_enabled = attrgetter("enabled") |
96 | 97 |
|
97 | 98 |
|
| 99 | +class _LazyValueType: |
| 100 | + """ |
| 101 | + LazyValue is an abstract type for wrapper for lazy evaluation of signals. |
| 102 | +
|
| 103 | + LazyValue is intended for situations in which computation of outputs is |
| 104 | + reasonably fast, but we won't to compute it only if the output is connected |
| 105 | + to some input, in order to save memory. |
| 106 | +
|
| 107 | + Assume the widget has a method `commit` that outputs the sum of two objects, |
| 108 | + `self.a` and `self.b`. The output signal is names `signal_name` and its |
| 109 | + type is `SomeType`. |
| 110 | +
|
| 111 | + ``` |
| 112 | + def commit(self): |
| 113 | + self.send(self.Outputs.signal_name, self.a + self.b) |
| 114 | + ``` |
| 115 | +
|
| 116 | + To use lazy values, we modify the method as follows. |
| 117 | +
|
| 118 | + ``` |
| 119 | + def commit(self): |
| 120 | + def f(): |
| 121 | + return self.a + self.b |
| 122 | +
|
| 123 | + self.send(self.Outputs.signal_name, LazySignal[SomeType](f)) |
| 124 | + ``` |
| 125 | +
|
| 126 | + The lazy function receives no arguments, so `commit` will often prepare |
| 127 | + some data accessible through closure or default arguments. After calling |
| 128 | + the function, LazyValue will release the reference to function, which in |
| 129 | + turn releases references to any data from closure or arguments. |
| 130 | +
|
| 131 | + LazyValue is a singleton, used in similar way as generic classes from |
| 132 | + typing. "Indexing" returns an instance of (internal) class `LazyValue_`. |
| 133 | + Indexing is cached; `LazyValue[SomeType]` always returns the same object. |
| 134 | +
|
| 135 | + LazySignal[SomeType] (that is: LazyValue_) has a constructor that expects |
| 136 | + the following arguments. |
| 137 | +
|
| 138 | + - A function that computes the actual value. This function must expect |
| 139 | + no arguments, but will usually get data (for instance `self`, in the |
| 140 | + above example) from closure. |
| 141 | + - An optional function that can be called to interrupt the computation. |
| 142 | + This function is called when the signal is deleted. |
| 143 | + - Optional extra arguments that are stored as LazyValue's attributes. |
| 144 | + These are not accessible by the above function and are primarily |
| 145 | + intended to be used in output summaries. |
| 146 | +
|
| 147 | + Properties: |
| 148 | +
|
| 149 | + - `is_cached()`, which returns `True` if the value is already computed. |
| 150 | + Functions for output summaries can use this to show more information |
| 151 | + if the value is available, and avoid computing it when not. |
| 152 | +
|
| 153 | + Methods: |
| 154 | +
|
| 155 | + - `get_value()` returns the actual value by calling the function, if |
| 156 | + the value has not been computed yet, or providing the cached value. |
| 157 | + - `type()` returns the type of the lazy signal (e.g. `SomeType`, in |
| 158 | + above case. |
| 159 | + """ |
| 160 | + |
| 161 | + class LazyValueMeta(type): |
| 162 | + def __repr__(cls): |
| 163 | + """ |
| 164 | + Pretty-prints the LazyValue[SomeType] as "LazyValue[SomeType]" |
| 165 | + instead of generic `LazyValue_`. |
| 166 | + """ |
| 167 | + return f"LazyValue[{cls.type().__name__}]" |
| 168 | + |
| 169 | + @classmethod |
| 170 | + def is_lazy(cls, value): |
| 171 | + """ |
| 172 | + Tells whether the given value is lazy. |
| 173 | +
|
| 174 | + ``` |
| 175 | + >>> def f(): |
| 176 | + ... return 12 |
| 177 | + ... |
| 178 | + >>> lazy = LazyValue[int](f) |
| 179 | + >>> eager = f() |
| 180 | + >>> LazyValue.is_lazy(lazy) |
| 181 | + True |
| 182 | + >>> LazyValue.is_lazy(eager) |
| 183 | + False |
| 184 | + ``` |
| 185 | + """ |
| 186 | + return isinstance(type(value), cls.LazyValueMeta) |
| 187 | + |
| 188 | + @classmethod |
| 189 | + @functools.lru_cache(maxsize=None) |
| 190 | + def __getitem__(cls, type_): |
| 191 | + # This is cached, so that it always returns the same class for the |
| 192 | + # same type. |
| 193 | + # >>> t1 = LazyValue[int] |
| 194 | + # >>> t2 = LazyValue[int] |
| 195 | + # >>> t1 is t2 |
| 196 | + # True |
| 197 | + class LazyValue_(metaclass=cls.LazyValueMeta): |
| 198 | + __type = type_ |
| 199 | + |
| 200 | + def __init__(self, func: Callable, interrupt=None, **extra_attrs): |
| 201 | + self.__func = func |
| 202 | + self.__cached = None |
| 203 | + self.interrupt = interrupt |
| 204 | + self.__dict__.update(extra_attrs) |
| 205 | + |
| 206 | + def __del__(self): |
| 207 | + if self.interrupt is not None: |
| 208 | + self.interrupt() |
| 209 | + |
| 210 | + @property |
| 211 | + def is_cached(self): |
| 212 | + return self.__func is None |
| 213 | + |
| 214 | + @classmethod |
| 215 | + def type(cls): |
| 216 | + return cls.__type |
| 217 | + |
| 218 | + def get_value(self): |
| 219 | + if self.__func is not None: |
| 220 | + self.__cached = self.__func() |
| 221 | + # This frees any references to closure and arguments |
| 222 | + self.__func = None |
| 223 | + return self.__cached |
| 224 | + |
| 225 | + return LazyValue_ |
| 226 | + |
| 227 | + |
| 228 | +LazyValue = _LazyValueType() |
| 229 | + |
| 230 | + |
98 | 231 | class _OutputState: |
99 | 232 | """Output state for a single node/channel""" |
100 | 233 | __slots__ = ('flags', 'outputs') |
|
0 commit comments