Skip to content

Commit c0cec14

Browse files
committed
DROP op.PARAMS - use funtools.partial() instead...
was letting complex graph designs with constants hard to find in nested code.
1 parent 1560990 commit c0cec14

File tree

7 files changed

+113
-68
lines changed

7 files changed

+113
-68
lines changed

README.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ produces multiple outputs (``a * b``, ``a - a * b``, and
4141
``abs(a - a * b) ** 3``)::
4242

4343
>>> from operator import mul, sub
44+
>>> from functools import partial
4445
>>> from graphtik import compose, operation
4546

4647
>>> # Computes |a|^p.
@@ -53,7 +54,8 @@ Compose the ``mul``, ``sub``, and ``abspow`` functions into a computation graph:
5354
>>> graphop = compose(name="graphop")(
5455
... operation(name="mul1", needs=["a", "b"], provides=["ab"])(mul),
5556
... operation(name="sub1", needs=["a", "ab"], provides=["a_minus_ab"])(sub),
56-
... operation(name="abspow1", needs=["a_minus_ab"], provides=["abs_a_minus_ab_cubed"], params={"p": 3})(abspow)
57+
... operation(name="abspow1", needs=["a_minus_ab"], provides=["abs_a_minus_ab_cubed"])
58+
... (partial(abspow, p=3))
5759
... )
5860

5961

docs/source/composition.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Simple composition of operations
2323
The simplest use case for ``compose`` is assembling a collection of individual operations into a runnable computation graph. The example script from :ref:`quick-start` illustrates this well::
2424

2525
>>> from operator import mul, sub
26+
>>> from functools import partial
2627
>>> from graphtik import compose, operation
2728

2829
>>> # Computes |a|^p.
@@ -34,7 +35,8 @@ The simplest use case for ``compose`` is assembling a collection of individual o
3435
>>> graphop = compose(name="graphop")(
3536
... operation(name="mul1", needs=["a", "b"], provides=["ab"])(mul),
3637
... operation(name="sub1", needs=["a", "ab"], provides=["a_minus_ab"])(sub),
37-
... operation(name="abspow1", needs=["a_minus_ab"], provides=["abs_a_minus_ab_cubed"], params={"p": 3})(abspow)
38+
... operation(name="abspow1", needs=["a_minus_ab"], provides=["abs_a_minus_ab_cubed"])
39+
... (partial(abspow, p=3))
3840
... )
3941

4042
The call here to ``compose()`` yields a runnable computation graph that looks like this (where the circles are operations, squares are data, and octagons are parameters):

docs/source/index.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ OR with dependencies for plotting support (and you need to install `Graphviz
5454
Here's a Python script with an example Graphtik computation graph that produces multiple outputs (``a * b``, ``a - a * b``, and ``abs(a - a * b) ** 3``)::
5555

5656
>>> from operator import mul, sub
57+
>>> from functools import partial
5758
>>> from graphtik import compose, operation
5859

5960
# Computes |a|^p.
@@ -66,7 +67,8 @@ Compose the ``mul``, ``sub``, and ``abspow`` functions into a computation graph:
6667
>>> graphop = compose(name="graphop")(
6768
... operation(name="mul1", needs=["a", "b"], provides=["ab"])(mul),
6869
... operation(name="sub1", needs=["a", "ab"], provides=["a_minus_ab"])(sub),
69-
... operation(name="abspow1", needs=["a_minus_ab"], provides=["abs_a_minus_ab_cubed"], params={"p": 3})(abspow)
70+
... operation(name="abspow1", needs=["a_minus_ab"], provides=["abs_a_minus_ab_cubed"])
71+
... (partial(abspow, p=3))
7072
... )
7173

7274
Run the graph-operation and request all of the outputs::

docs/source/operations.rst

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ When many operations are composed into a computation graph (see :ref:`graph-comp
4444
Let's look again at the operations from the script in :ref:`quick-start`, for example::
4545

4646
>>> from operator import mul, sub
47+
>>> from functools import partial
4748
>>> from graphtik import compose, operation
4849

4950
>>> # Computes |a|^p.
@@ -55,25 +56,23 @@ Let's look again at the operations from the script in :ref:`quick-start`, for ex
5556
>>> graphop = compose(name="graphop")(
5657
... operation(name="mul1", needs=["a", "b"], provides=["ab"])(mul),
5758
... operation(name="sub1", needs=["a", "ab"], provides=["a_minus_ab"])(sub),
58-
... operation(name="abspow1", needs=["a_minus_ab"], provides=["abs_a_minus_ab_cubed"], params={"p": 3})(abspow)
59+
... operation(name="abspow1", needs=["a_minus_ab"], provides=["abs_a_minus_ab_cubed"])
60+
... (partial(abspow, p=3))
5961
... )
6062

61-
The ``needs`` and ``provides`` arguments to the operations in this script define a computation graph that looks like this (where the oval are operations, squares/houses are data):
63+
.. Tip::
64+
Notice the use of :func:`functools.partial()` to set parameter ``p`` to a contant value.
65+
66+
The ``needs`` and ``provides`` arguments to the operations in this script define
67+
a computation graph that looks like this (where the oval are *operations*,
68+
squares/houses are *data*):
6269

6370
.. image:: images/barebone_3ops.svg
6471

6572
.. Tip::
6673
See :ref:`plotting` on how to make diagrams like this.
6774

6875

69-
Constant operation parameters: ``params``
70-
-----------------------------------------
71-
72-
Sometimes an ``operation`` will have a customizable parameter you want to hold constant across all runs of a computation graph. Usually, this will be a keyword argument of the underlying function. The ``params`` argument to the ``operation`` constructor provides a mechanism for setting such parameters.
73-
74-
``params`` should be a dictionary whose keys correspond to keyword parameter names from the function underlying an ``operation`` and whose values are passed as constant arguments to those keyword parameters in all computations utilizing the ``operation``.
75-
76-
7776
Instantiating operations
7877
------------------------
7978

@@ -105,31 +104,36 @@ If the functions underlying your computation graph operations are defined elsewh
105104

106105
>>> graphop = compose(name='add_mul_graph')(add_op, mul_op)
107106

108-
The functional specification is also useful if you want to create multiple ``operation`` instances from the same function, perhaps with different parameter values, e.g.::
107+
The functional specification is also useful if you want to create multiple ``operation``
108+
instances from the same function, perhaps with different parameter values, e.g.::
109109

110-
>>> from graphtik import operation, compose
110+
>>> from functools import partial
111111

112112
>>> def mypow(a, p=2):
113113
... return a ** p
114114

115115
>>> pow_op1 = operation(name='pow_op1', needs=['a'], provides='a_squared')(mypow)
116-
>>> pow_op2 = operation(name='pow_op2', needs=['a'], params={'p': 3}, provides='a_cubed')(mypow)
116+
>>> pow_op2 = operation(name='pow_op2', needs=['a'], provides='a_cubed')(partial(mypow, p=3))
117117

118118
>>> graphop = compose(name='two_pows_graph')(pow_op1, pow_op2)
119119

120120
A slightly different approach can be used here to accomplish the same effect by creating an operation "factory"::
121121

122-
from graphtik import operation, compose
122+
>>> def mypow(a, p=2):
123+
... return a ** p
123124

124-
def mypow(a, p=2):
125-
return a ** p
125+
>>> pow_op_factory = operation(mypow, needs=['a'], provides='a_squared')
126126

127-
pow_op_factory = operation(mypow)
127+
>>> pow_op1 = pow_op_factory(name='pow_op1')
128+
>>> pow_op2 = pow_op_factory(name='pow_op2', provides='a_cubed', fn=partial(mypow, p=3))
128129

129-
pow_op1 = pow_op_factory(name='pow_op1', needs=['a'], provides='a_squared')
130-
pow_op2 = pow_op_factory(name='pow_op2', needs=['a'], params={'p': 3}, provides='a_cubed')
130+
>>> graphop = compose(name='two_pows_graph')(pow_op1, pow_op2)
131+
>>> graphop({'a': 2})
132+
{'a': 2, 'a_cubed': 8, 'a_squared': 4}
131133

132-
graphop = compose(name='two_pows_graph')(pow_op1, pow_op2)
134+
.. Note::
135+
You cannot call again the factory to overwrite the *function*,
136+
you have to use the ``fn=`` keyword.
133137

134138

135139
Modifiers on ``operation`` inputs and outputs

graphtik/base.py

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,7 @@
1414
log = logging.getLogger(__name__)
1515

1616

17-
def jetsam(
18-
ex, locs, *salvage_vars: str, annotation="jetsam", **salvage_mappings
19-
):
17+
def jetsam(ex, locs, *salvage_vars: str, annotation="jetsam", **salvage_mappings):
2018
"""
2119
Annotate exception with salvaged values from locals() and raise!
2220
@@ -122,7 +120,7 @@ def jetsam(
122120
class Operation(object):
123121
"""An abstract class representing a data transformation by :meth:`.compute()`."""
124122

125-
def __init__(self, name=None, needs=None, provides=None, params=None, **kwargs):
123+
def __init__(self, name=None, needs=None, provides=None, **kwargs):
126124
"""
127125
Create a new layer instance.
128126
Names may be given to this layer and its inputs and outputs. This is
@@ -138,19 +136,12 @@ def __init__(self, name=None, needs=None, provides=None, params=None, **kwargs):
138136
:param list provides:
139137
Names of output data objects this provides.
140138
141-
:param dict params:
142-
A dict of key/value pairs representing parameters
143-
associated with your operation. These values will be
144-
accessible using the ``.params`` attribute of your object.
145-
NOTE: It's important that any values stored in this
146-
argument must be pickelable.
147139
"""
148140

149141
# (Optional) names for this layer, and the data it needs and provides
150142
self.name = name
151143
self.needs = needs
152144
self.provides = provides
153-
self.params = params or {}
154145

155146
# call _after_init as final step of initialization
156147
self._after_init()
@@ -186,8 +177,8 @@ def compute(self, named_inputs, outputs=None):
186177
def _after_init(self):
187178
"""
188179
This method is a hook for you to override. It gets called after this
189-
object has been initialized with its ``needs``, ``provides``, ``name``,
190-
and ``params`` attributes. People often override this method to implement
180+
object has been initialized with its ``name``, ``needs`` and ``provides``
181+
attributes. People often override this method to implement
191182
custom loading logic required for objects that do not pickle easily, and
192183
for initialization of c++ dependencies.
193184
"""

graphtik/functional.py

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,11 @@ def compute(self, named_inputs, outputs=None):
2828
for n in self.needs
2929
if isinstance(n, optional) and n in named_inputs
3030
}
31-
# Combine params and optionals into one big glob of keyword arguments.
32-
kwargs = {k: v for d in (self.params, optionals) for k, v in d.items()}
3331

3432
# Don't expect sideffect outputs.
3533
provides = [n for n in self.provides if not isinstance(n, sideffect)]
3634

37-
results = self.fn(*args, **kwargs)
35+
results = self.fn(*args, **optionals)
3836

3937
if not provides:
4038
# All outputs were sideffects.
@@ -59,7 +57,7 @@ def compute(self, named_inputs, outputs=None):
5957
operation="self",
6058
args=lambda locs: {
6159
"args": locs.get("args"),
62-
"kwargs": locs.get("kwargs"),
60+
"kwargs": locs.get("optionals"),
6361
},
6462
)
6563

@@ -88,10 +86,6 @@ class operation(Operation):
8886
:param list provides:
8987
Names of output data objects this operation provides.
9088
91-
:param dict params:
92-
A dict of key/value pairs representing constant parameters
93-
associated with your operation. These can correspond to either
94-
``args`` or ``kwargs`` of ``fn``.
9589
"""
9690

9791
def __init__(self, fn=None, **kwargs):
@@ -119,9 +113,6 @@ def _normalize_kwargs(self, kwargs):
119113
kwargs["fn"], "__call__"
120114
), "operation was not provided with a callable"
121115

122-
if type(kwargs["params"]) is not dict:
123-
kwargs["params"] = {}
124-
125116
return kwargs
126117

127118
def __call__(self, fn=None, **kwargs):
@@ -241,5 +232,5 @@ def __call__(self, *operations, **kwargs):
241232
net.add_op(op)
242233

243234
return NetworkOperation(
244-
net, name=self.name, needs=needs, provides=provides, params={}, **kwargs
235+
net, name=self.name, needs=needs, provides=provides, **kwargs
245236
)

test/test_graphtik.py

Lines changed: 73 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import math
55
import sys
6+
from functools import partial
67
from operator import add, floordiv, mul, sub
78
from pprint import pprint
89

@@ -56,12 +57,9 @@ def mul_op1(a, b):
5657

5758
# Pow operation
5859
@operation(
59-
name="pow_op1",
60-
needs="sum_ab",
61-
provides=["sum_ab_p1", "sum_ab_p2", "sum_ab_p3"],
62-
params={"exponent": 3},
60+
name="pow_op1", needs="sum_ab", provides=["sum_ab_p1", "sum_ab_p2", "sum_ab_p3"]
6361
)
64-
def pow_op1(a, exponent=2):
62+
def pow_op1(a, exponent=3):
6563
return [math.pow(a, y) for y in range(1, exponent + 1)]
6664

6765
assert pow_op1.compute({"sum_ab": 2}, ["sum_ab_p2"]) == {"sum_ab_p2": 4.0}
@@ -117,6 +115,70 @@ def pow_op1(a, exponent=2):
117115
netop({"sum_ab": 1, "b": 2}, outputs=["b", "bad_node"])
118116

119117

118+
def test_network_plan_execute():
119+
def powers_in_trange(a, exponent):
120+
outputs = []
121+
for y in range(1, exponent + 1):
122+
p = math.pow(a, y)
123+
outputs.append(p)
124+
return outputs
125+
126+
sum_op1 = operation(name="sum1", provides=["sum_ab"], needs=["a", "b"])(add)
127+
mul_op1 = operation(name="mul", provides=["sum_ab_times_b"], needs=["sum_ab", "b"])(
128+
mul
129+
)
130+
pow_op1 = operation(
131+
name="pow",
132+
needs=["sum_ab", "exponent"],
133+
provides=["sum_ab_p1", "sum_ab_p2", "sum_ab_p3"],
134+
)(powers_in_trange)
135+
sum_op2 = operation(
136+
name="sum2", provides=["p1_plus_p2"], needs=["sum_ab_p1", "sum_ab_p2"]
137+
)(add)
138+
139+
net = network.Network()
140+
net.add_op(sum_op1)
141+
net.add_op(mul_op1)
142+
net.add_op(pow_op1)
143+
net.add_op(sum_op2)
144+
net.compile()
145+
146+
#
147+
# Running the network
148+
#
149+
150+
# get all outputs
151+
exp = {
152+
"a": 1,
153+
"b": 2,
154+
"exponent": 3,
155+
"p1_plus_p2": 12.0,
156+
"sum_ab": 3,
157+
"sum_ab_p1": 3.0,
158+
"sum_ab_p2": 9.0,
159+
"sum_ab_p3": 27.0,
160+
"sum_ab_times_b": 6,
161+
}
162+
163+
inputs = {"a": 1, "b": 2, "exponent": 3}
164+
plan = net.compile(outputs=None, inputs=inputs.keys())
165+
sol = plan.execute(named_inputs=inputs)
166+
assert sol == exp
167+
168+
# get specific outputs
169+
exp = {"sum_ab_times_b": 6}
170+
plan = net.compile(outputs=["sum_ab_times_b"], inputs=list(inputs))
171+
sol = plan.execute(named_inputs=inputs)
172+
assert sol == exp
173+
174+
# start with inputs already computed
175+
inputs = {"sum_ab": 1, "b": 2, "exponent": 3}
176+
exp = {"sum_ab_times_b": 2}
177+
plan = net.compile(outputs=["sum_ab_times_b"], inputs=inputs)
178+
sol = plan.execute(named_inputs={"sum_ab": 1, "b": 2})
179+
assert sol == exp
180+
181+
120182
def test_network_simple_merge():
121183

122184
sum_op1 = operation(name="sum_op1", needs=["a", "b"], provides="sum1")(add)
@@ -178,11 +240,8 @@ def test_network_merge_in_doctests():
178240
operation(name="mul1", needs=["a", "b"], provides=["ab"])(mul),
179241
operation(name="sub1", needs=["a", "ab"], provides=["a_minus_ab"])(sub),
180242
operation(
181-
name="abspow1",
182-
needs=["a_minus_ab"],
183-
provides=["abs_a_minus_ab_cubed"],
184-
params={"p": 3},
185-
)(abspow),
243+
name="abspow1", needs=["a_minus_ab"], provides=["abs_a_minus_ab_cubed"]
244+
)(partial(abspow, p=3)),
186245
)
187246

188247
another_graph = compose(name="another_graph")(
@@ -853,11 +912,8 @@ def test_multithreading_plan_execution():
853912
operation(name="mul1", needs=["a", "b"], provides=["ab"])(mul),
854913
operation(name="sub1", needs=["a", "ab"], provides=["a_minus_ab"])(sub),
855914
operation(
856-
name="abspow1",
857-
needs=["a_minus_ab"],
858-
provides=["abs_a_minus_ab_cubed"],
859-
params={"p": 3},
860-
)(abspow),
915+
name="abspow1", needs=["a_minus_ab"], provides=["abs_a_minus_ab_cubed"]
916+
)(partial(abspow, p=3)),
861917
)
862918

863919
pool = Pool(10)
@@ -969,11 +1025,8 @@ def test_compose_another_network(bools):
9691025
operation(name="mul1", needs=["a", "b"], provides=["ab"])(mul),
9701026
operation(name="sub1", needs=["a", "ab"], provides=["a_minus_ab"])(sub),
9711027
operation(
972-
name="abspow1",
973-
needs=["a_minus_ab"],
974-
provides=["abs_a_minus_ab_cubed"],
975-
params={"p": 3},
976-
)(abspow),
1028+
name="abspow1", needs=["a_minus_ab"], provides=["abs_a_minus_ab_cubed"]
1029+
)(partial(abspow, p=3)),
9771030
)
9781031
if parallel1:
9791032
graphop.set_execution_method("parallel")

0 commit comments

Comments
 (0)