Skip to content

Commit d75a754

Browse files
Merge pull request #70 from brandonwillard/cache-hash-values
Make better use of slots and cache hash values
2 parents 9743bcd + 209b90a commit d75a754

File tree

19 files changed

+1197
-628
lines changed

19 files changed

+1197
-628
lines changed

requirements-dev.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ pytest-cov>=2.6.1
44
pytest-html>=1.20.0
55
pylint>=2.3.1
66
black>=19.3b0
7+
ipython

requirements.txt

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
scipy>=1.2.0
22
Theano>=1.0.4
3-
gast==0.2.2
4-
tf-nightly-2.0-preview==2.0.0.dev20190908
5-
tensorflow-estimator-2.0-preview==1.14.0.dev2019090801
6-
tfp-nightly==0.9.0.dev20190908
7-
pymc3>=3.6
83
pymc4 @ git+https://github.com/pymc-devs/pymc4.git@master#egg=pymc4-0.0.1
4+
tfp-nightly==0.9.0.dev20191003
5+
tf-nightly-2.0-preview==2.0.0.dev20191002
6+
tensorflow-estimator-2.0-preview>=1.14.0.dev2019090801
7+
pymc3>=3.6
98
multipledispatch>=0.6.0
109
unification>=0.2.2
1110
kanren @ git+https://github.com/pymc-devs/kanren.git@symbolic-pymc#egg=kanren-0.2.3
11+
cons>=0.1.3
1212
toolz>=0.9.0
1313
sympy>=1.3
14+
cachetools

setup.cfg

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,7 @@ add-ignore = D100,D101,D102,D103,D104,D105,D106,D107,D202
66
convention = numpy
77

88
[tool:pytest]
9-
python_functions=test_*
9+
python_functions=test_*
10+
filterwarnings =
11+
ignore:the imp module is deprecated:DeprecationWarning:
12+
ignore:Using a non-tuple sequence:FutureWarning:theano\.tensor

symbolic_pymc/etuple.py

Lines changed: 137 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
import toolz
55

6+
from collections import Sequence
7+
68
from multipledispatch import dispatch
79

810
from kanren.term import operator, arguments
@@ -13,64 +15,108 @@
1315
etuple_repr.maxother = 100
1416

1517

16-
class KwdPair(tuple):
18+
class KwdPair(object):
1719
"""A class used to indicate a keyword + value mapping.
1820
1921
TODO: Could subclass `ast.keyword`.
2022
2123
"""
2224

23-
def __new__(cls, arg, value):
25+
__slots__ = ("arg", "value")
26+
27+
def __init__(self, arg, value):
2428
assert isinstance(arg, str)
25-
obj = super().__new__(cls, (arg, value))
26-
return obj
29+
self.arg = arg
30+
self.value = value
2731

2832
@property
2933
def eval_obj(self):
30-
return KwdPair(self[0], getattr(self[1], "eval_obj", self[1]))
34+
return KwdPair(self.arg, getattr(self.value, "eval_obj", self.value))
3135

3236
def __repr__(self):
33-
return f"{str(self[0])}={repr(self[1])}"
37+
return f"{self.__class__.__name__}({repr(self.arg)}, {repr(self.value)})"
38+
39+
def __str__(self):
40+
return f"{self.arg}={self.value}"
41+
42+
def _repr_pretty_(self, p, cycle):
43+
p.text(str(self))
3444

3545

36-
class ExpressionTuple(tuple):
37-
"""A tuple object that represents an expression.
46+
class ExpressionTuple(Sequence):
47+
"""A tuple-like object that represents an expression.
3848
39-
This object carries the underlying object, if any, and preserves it
40-
through limited forms of concatenation/cons-ing.
49+
This object caches the return value resulting from evaluation of the
50+
expression it represents. Likewise, it holds onto the "parent" expression
51+
from which it was derived (e.g. as a slice), if any, so that it can
52+
preserve the return value through limited forms of concatenation/cons-ing
53+
that would reproduce the parent expression.
4154
55+
TODO: Should probably use weakrefs for that.
4256
"""
4357

58+
__slots__ = ("_eval_obj", "_tuple", "_orig_expr")
4459
null = object()
4560

46-
def __new__(cls, *args, **kwargs):
47-
obj = super().__new__(cls, *args, **kwargs)
48-
# TODO: Consider making this a weakref.
49-
obj._eval_obj = cls.null
50-
return obj
61+
def __new__(cls, seq=None, **kwargs):
62+
63+
# XXX: This doesn't actually remove the entry from the kwargs
64+
# passed to __init__!
65+
# It does, however, remove it for the check below.
66+
kwargs.pop("eval_obj", None)
67+
68+
if seq is None and not kwargs and isinstance(seq, cls):
69+
return seq
70+
71+
res = super().__new__(cls)
72+
73+
return res
74+
75+
def __init__(self, seq=None, **kwargs):
76+
"""Create an expression tuple.
77+
78+
If the keyword 'eval_obj' is given, the `ExpressionTuple`'s
79+
evaluated object is set to the corresponding value.
80+
XXX: There is no verification/check that the arguments evaluate to the
81+
user-specified 'eval_obj', so be careful.
82+
"""
83+
84+
_eval_obj = kwargs.pop("eval_obj", self.null)
85+
etuple_kwargs = tuple(KwdPair(k, v) for k, v in kwargs.items())
86+
87+
if seq:
88+
self._tuple = tuple(seq) + etuple_kwargs
89+
else:
90+
self._tuple = etuple_kwargs
91+
92+
# TODO: Consider making these a weakrefs.
93+
self._eval_obj = _eval_obj
94+
self._orig_expr = None
5195

5296
@property
5397
def eval_obj(self):
5498
"""Return the evaluation of this expression tuple.
5599
56-
XXX: If the object isn't cached, it will be evaluated recursively.
100+
Warning: If the evaluation value isn't cached, it will be evaluated
101+
recursively.
57102
58103
"""
59-
if self._eval_obj is not ExpressionTuple.null:
104+
if self._eval_obj is not self.null:
60105
return self._eval_obj
61106
else:
62-
evaled_args = [getattr(i, "eval_obj", i) for i in self[1:]]
107+
evaled_args = [getattr(i, "eval_obj", i) for i in self._tuple[1:]]
63108
arg_grps = toolz.groupby(lambda x: isinstance(x, KwdPair), evaled_args)
64109
evaled_args = arg_grps.get(False, [])
65110
evaled_kwargs = arg_grps.get(True, [])
66111

67-
op = self[0]
112+
op = self._tuple[0]
68113
try:
69114
op_sig = inspect.signature(op)
70115
except ValueError:
71-
_eval_obj = op(*(evaled_args + [kw[1] for kw in evaled_kwargs]))
116+
# This handles some builtin function types
117+
_eval_obj = op(*(evaled_args + [kw.value for kw in evaled_kwargs]))
72118
else:
73-
op_args = op_sig.bind(*evaled_args, **dict(evaled_kwargs))
119+
op_args = op_sig.bind(*evaled_args, **{kw.arg: kw.value for kw in evaled_kwargs})
74120
op_args.apply_defaults()
75121

76122
_eval_obj = op(*op_args.args, **op_args.kwargs)
@@ -84,87 +130,104 @@ def eval_obj(self):
84130
def eval_obj(self, obj):
85131
raise ValueError("Value of evaluated expression cannot be set!")
86132

133+
def __add__(self, x):
134+
res = self._tuple + x
135+
if self._orig_expr is not None and res == self._orig_expr._tuple:
136+
return self._orig_expr
137+
return type(self)(res)
138+
139+
def __contains__(self, *args):
140+
return self._tuple.__contains__(*args)
141+
142+
def __ge__(self, *args):
143+
return self._tuple.__ge__(*args)
144+
87145
def __getitem__(self, key):
88-
# if isinstance(key, slice):
89-
# return [self.list[i] for i in xrange(key.start, key.stop, key.step)]
90-
# return self.list[key]
91-
tuple_res = super().__getitem__(key)
146+
tuple_res = self._tuple[key]
92147
if isinstance(key, slice) and isinstance(tuple_res, tuple):
93148
tuple_res = type(self)(tuple_res)
94-
tuple_res.orig_expr = self
149+
tuple_res._orig_expr = self
95150
return tuple_res
96151

97-
def __add__(self, x):
98-
res = type(self)(super().__add__(x))
99-
if res == getattr(self, "orig_expr", None):
100-
return self.orig_expr
101-
return res
152+
def __gt__(self, *args):
153+
return self._tuple.__gt__(*args)
154+
155+
def __iter__(self, *args):
156+
return self._tuple.__iter__(*args)
157+
158+
def __le__(self, *args):
159+
return self._tuple.__le__(*args)
160+
161+
def __len__(self, *args):
162+
return self._tuple.__len__(*args)
163+
164+
def __lt__(self, *args):
165+
return self._tuple.__lt__(*args)
166+
167+
def __mul__(self, *args):
168+
return self._tuple.__mul__(*args)
169+
170+
def __rmul__(self, *args):
171+
return self._tuple.__rmul__(*args)
102172

103173
def __radd__(self, x):
104-
res = type(self)(x + tuple(self))
105-
if res == getattr(self, "orig_expr", None):
106-
return self.orig_expr
107-
return res
174+
res = x + self._tuple # type(self)(x + self._tuple)
175+
if self._orig_expr is not None and res == self._orig_expr._tuple:
176+
return self._orig_expr
177+
return type(self)(res)
108178

109179
def __str__(self):
110-
return f"e({', '.join(tuple(str(i) for i in self))})"
180+
return f"e({', '.join(tuple(str(i) for i in self._tuple))})"
111181

112182
def __repr__(self):
113-
return f"ExpressionTuple({etuple_repr.repr(tuple(self))})"
183+
return f"ExpressionTuple({etuple_repr.repr(self._tuple)})"
114184

115185
def _repr_pretty_(self, p, cycle):
116186
if cycle:
117-
p.text(f"{self.__class__.__name__}(...)")
187+
p.text(f"e(...)")
118188
else:
119-
with p.group(2, f"{self.__class__.__name__}((", "))"):
120-
p.breakable()
121-
for idx, item in enumerate(self):
189+
with p.group(2, "e(", ")"):
190+
p.breakable(sep="")
191+
for idx, item in enumerate(self._tuple):
122192
if idx:
123193
p.text(",")
124194
p.breakable()
125195
p.pretty(item)
126196

197+
def __eq__(self, other):
198+
return self._tuple == other
127199

128-
def etuple(*args, **kwargs):
129-
"""Create an expression tuple from the arguments.
200+
def __hash__(self):
201+
return hash(self._tuple)
130202

131-
If the keyword 'eval_obj' is given, the `ExpressionTuple`'s
132-
evaluated object is set to the corresponding value.
133-
XXX: There is no verification/check that the arguments evaluate to the
134-
user-specified 'eval_obj', so be careful.
135203

136-
"""
137-
_eval_obj = kwargs.pop("eval_obj", ExpressionTuple.null)
138-
139-
etuple_kwargs = tuple(KwdPair(k, v) for k, v in kwargs.items())
140-
141-
res = ExpressionTuple(args + etuple_kwargs)
204+
def etuple(*args, **kwargs):
205+
"""Create an ExpressionTuple from the argument list.
142206
143-
res._eval_obj = _eval_obj
207+
In other words:
208+
etuple(1, 2, 3) == ExpressionTuple((1, 2, 3))
144209
145-
return res
210+
"""
211+
return ExpressionTuple(args, **kwargs)
146212

147213

148214
@dispatch(object)
149-
def etuplize(x, shallow=False):
215+
def etuplize(x, shallow=False, return_bad_args=False):
150216
"""Return an expression-tuple for an object (i.e. a tuple of rand and rators).
151217
152-
When evaluated, the rand and rators should [re-]construct the object. When the
153-
object cannot be given such a form, the object itself is returned.
154-
155-
NOTE: `etuplize(...)[2:]` and `arguments(...)` will *not* return
156-
the same thing by default, because the former is recursive and the latter
157-
is not. In other words, this S-expression-like "decomposition" is
158-
recursive, and, as such, it requires an inside-out evaluation to
159-
re-construct a "decomposed" object. In contrast, `operator` and
160-
`arguments` is necessarily a shallow "decomposition".
218+
When evaluated, the rand and rators should [re-]construct the object. When
219+
the object cannot be given such a form, it is simply converted to an
220+
`ExpressionTuple` and returned.
161221
162222
Parameters
163223
----------
164224
x: object
165225
Object to convert to expression-tuple form.
166226
shallow: bool
167227
Whether or not to do a shallow conversion.
228+
return_bad_args: bool
229+
Return the passed argument when its type is not appropriate, instead
230+
of raising an exception.
168231
169232
"""
170233
if isinstance(x, ExpressionTuple):
@@ -176,18 +239,23 @@ def etuplize(x, shallow=False):
176239
op = operator(x)
177240
args = arguments(x)
178241
except (IndexError, NotImplementedError):
179-
return x
242+
op = None
243+
args = x
180244

181-
assert isinstance(args, (list, tuple))
245+
if not isinstance(args, Sequence) or isinstance(args, str):
246+
if return_bad_args:
247+
return x
248+
else:
249+
raise TypeError(f"x is neither a non-str Sequence nor term: {type(x)}")
182250

183251
# Not everything in a list/tuple should be considered an expression.
184252
if not callable(op):
185-
return x
253+
return etuple(*x)
186254

187255
if shallow:
188256
et_args = args
189257
else:
190-
et_args = tuple(etuplize(a) for a in args)
258+
et_args = tuple(etuplize(a, return_bad_args=True) for a in args)
191259

192260
res = etuple(op, *et_args, eval_obj=x)
193261
return res

0 commit comments

Comments
 (0)