i2.routing_forest¶
Tools to specify functions through trees and forests.
Whaaa?!?
Well, you see, often – especially when writing transformers – you have a series of if/then conditions nested into eachother, in code, where it gets ugly and un-reusable.
This module explores ways to objectivy this: That is, to give us the means to create such nested conditions in a way that we can define the parts as reusable operable components.
Think of the relationship between the for loop (code) and the iterator (object), along with iterator tools (itertools). This is what we’re trying to explore, but for if/then conditions.
I said explore. Some more work is needed here to make it robust and easily usable.
Let’s look at an example involving the three main actors of our play.
Each of these are Iterable and Callable (Generator to be precise).
CondNode: implements the if/then (no else) logicFinalNode: Final – yields (both with call and iter) it’s single.valattribute.RoutingForest: An Iterable ofCondNode
You’ll note that instances of these classes are all both callables and iterables,
and that when called, they return iterables.
It’s this aspect that makes us be able to nest conditions within conditions,
and further, control the flow of the iteration from outside.
A routing node (or forest) called on an object will yield all values that match the
conditions that were specified for it.
For example, if you need all matches, you can wrap it with list, if you need the
first match only, you can wrap it with next, if you have a default value,
you can wrap it in next with a default value.
>>> import inspect
>>>
>>> def could_be_int(obj):
... if isinstance(obj, int):
... b = True
... else:
... try:
... int(obj)
... b = True
... except ValueError:
... b = False
... if b:
... print(f'{inspect.currentframe().f_code.co_name}')
... return b
...
>>> def could_be_float(obj):
... if isinstance(obj, float):
... b = True
... else:
... try:
... float(obj)
... b = True
... except ValueError:
... b = False
... if b:
... print(f'{inspect.currentframe().f_code.co_name}')
... return b
...
>>> print(
... could_be_int(30),
... could_be_int(30.3),
... could_be_int('30.2'),
... could_be_int('nope'),
... )
could_be_int
could_be_int
True True False False
>>> print(
... could_be_float(30),
... could_be_float(30.3),
... could_be_float('30.2'),
... could_be_float('nope'),
... )
could_be_float
could_be_float
could_be_float
True True True False
>>> assert could_be_int('30.2') is False
>>> assert could_be_float('30.2') is True
could_be_float
>>>
>>> st = RoutingForest(
... [
... CondNode(
... cond=could_be_int,
... then=RoutingForest(
... [
... CondNode(
... cond=lambda x: int(x) >= 10,
... then=FinalNode('More than a digit'),
... ),
... CondNode(
... cond=lambda x: (int(x) % 2) == 1,
... then=FinalNode("That's odd!"),
... ),
... ]
... ),
... ),
... CondNode(cond=could_be_float, then=FinalNode('could be seen as a float')),
... ]
... )
>>> assert list(st('nothing I can do with that')) == []
>>> assert list(st(8)) == ['could be seen as a float']
could_be_int
could_be_float
>>> assert list(st(9)) == ["That's odd!", 'could be seen as a float']
could_be_int
could_be_float
>>> assert list(st(10)) == ['More than a digit', 'could be seen as a float']
could_be_int
could_be_float
>>> assert list(st(11)) == [
... 'More than a digit',
... "That's odd!",
... 'could be seen as a float',
... ]
could_be_int
could_be_float
>>>
>>> print(
... '### RoutingForest ########################################################################################'
... )
### RoutingForest ########################################################################################
>>> rf = RoutingForest(
... [
... SwitchCaseNode(
... switch=lambda x: x % 5,
... cases={0: FinalNode('zero_mod_5'), 1: FinalNode('one_mod_5')},
... default=FinalNode('default_mod_5'),
... ),
... SwitchCaseNode(
... switch=lambda x: x % 2,
... cases={0: FinalNode('even'), 1: FinalNode('odd')},
... default=FinalNode('that is not an int'),
... ),
... ]
... )
>>>
>>> assert list(rf(5)) == ['zero_mod_5', 'odd']
>>> assert list(rf(6)) == ['one_mod_5', 'even']
>>> assert list(rf(7)) == ['default_mod_5', 'odd']
>>> assert list(rf(8)) == ['default_mod_5', 'even']
>>> assert list(rf(10)) == ['zero_mod_5', 'even']
>>>
Functions
|
Return the input unchanged (the default leaf function). |
|
Return a constanc sentinel value when called. |
Exercise the routing nodes end to end (kept here as a runnable example). |
|
Yield the items of |
Classes
|
A RoutingNode that implements the if/then (no else) logic |
A mixin to delegate |
|
|
A RoutingNode that yields multiple routes, one for each of several conditions met, where the condition is computed implements computes a feature of the obj and according to an iterable of conditions on the feature. |
|
A RoutingNode that is final. |
|
Implements a switch-case-like mapping with a callable key function. |
|
|
|
|
A RoutingNode instance needs to be callable on a single object, yielding an iterable or a final value |
|
|
A RoutingNode that implements the switch/case/else logic. |
- class i2.routing_forest.CondNode(cond, then)[source]¶
Bases:
RoutingNodeA RoutingNode that implements the if/then (no else) logic
- class i2.routing_forest.DelegateToMappingAttrMixin[source]¶
Bases:
objectA mixin to delegate
Mappingmethods to a mapping attribute calledmapping
- class i2.routing_forest.FeatCondNode(feat, feat_cond_thens)[source]¶
Bases:
RoutingNodeA RoutingNode that yields multiple routes, one for each of several conditions met, where the condition is computed implements computes a feature of the obj and according to an iterable of conditions on the feature.
>>> fcn = FeatCondNode( ... feat=lambda x: x % 5, ... feat_cond_thens=[ ... (lambda x: x == 0, lambda x: 'zero_mod_5'), ... (lambda x: x == 1, lambda x: 'one_mod_5'), ... (lambda x: x == 2, lambda x: 'two_mod_5'), ... (lambda x: x == 3, lambda x: 'three_mod_5'), ... (lambda x: x == 4, lambda x: 'four_mod_5'), ... ] ... ) >>> assert list(fcn(0)) == ['zero_mod_5'] >>> assert list(fcn(1)) == ['one_mod_5'] >>> assert list(fcn(2)) == ['two_mod_5'] >>> assert list(fcn(3)) == ['three_mod_5'] >>> assert list(fcn(4)) == ['four_mod_5'] >>> assert list(fcn(5)) == ['zero_mod_5'] >>> assert list(fcn(6)) == ['one_mod_5']- classmethod from_feature_val_map(feat, feat_cond_thens)[source]¶
A FeatCondNode where the conditions are equality checks on the feature value
# >>> fvn = FeatCondNode.from_feature_val_map( # … feat=lambda x: x % 3, # … feat_cond_thens={ # … 0: lambda x: ‘zero_mod_3’, # … 1: lambda x: ‘one_mod_3’, # … 2: lambda x: ‘two_mod_3’, # … } # … ) # >>> list(fvn(0)) # # >>> assert list(fvn(0)) == [‘zero_mod_3’] # >>> assert list(fvn(1)) == [‘one_mod_3’] # >>> assert list(fvn(2)) == [‘two_mod_3’] #
- class i2.routing_forest.FinalNode(val)[source]¶
Bases:
RoutingNodeA RoutingNode that is final. It yields (both with call and iter) it’s single
.valattribute.
- class i2.routing_forest.KeyFuncMapping(mapping=None, key=<function identity>, default_factory=<function return_sentinel>)[source]¶
Bases:
DelegateToMappingAttrMixin,MutableMappingImplements a switch-case-like mapping with a callable key function.
The purpose of
KeyFuncMappingis to allow switch-case logic to be given as a plugin specification.>>> from i2.routing_forest import KeyFuncMapping >>> >>> get_extension = lambda x: x.split('.')[-1] >>> >>> data_type = KeyFuncMapping( ... {'csv': 'table', 'xls': 'table', 'wav': 'audio'}, key=get_extension ... )Calling a
KeyFuncMappinginstance will call thekeyfunction on the input, then look up the result in themapping.>>> data_type('my_file.csv') 'table' >>> data_type('another_file.xls') 'table' >>> data_type('sound.wav') 'audio'If the key is not found in the mapping, the
default_factoryis called with the input and the result is returned. The defaultdefault_factoryisreturn_sentinel, which by default returnsNone>>> assert data_type('poem.txt') is NoneNote that instances of
KeyFuncMappingare alsoMapping``s, so all ``Mappingmethods can be used.>>> list(data_type) ['csv', 'xls', 'wav'] >>> dict(data_type) {'csv': 'table', 'xls': 'table', 'wav': 'audio'}Including
update, which constitutes a convenient way to extend the mapping.>>> data_type.update(txt='text') >>> data_type('poem.txt') 'text'The
default_factorycan be set to any callable, including aKeyFuncMappingitself, which enables us to define anelsefor the switch-case logic that aKeyFuncMappingimplements. Say, for example, if no handled extension is found, we want to check the protocol of the input string instead. This is not only a new mapping, but also a new key function. We can do it as such:>>> get_protocol = lambda x: x.split('://')[0] >>> protocol = KeyFuncMapping({'https': 'url'}, get_protocol) >>> new_data_type = KeyFuncMapping( ... data_type.mapping, data_type.key, default_factory=protocol ... ) >>> new_data_type('notes.txt') 'text' >>> new_data_type('https://www.python.org/') 'url'Given how useful this pattern is, we made the
+operator implement this. Note that here,+is not associative or commutative (as with numbers). It should be understood to function more like the+for iterables likelistandtuple.>>> nested = data_type + protocol >>> nested('https://www.python.org/') 'url' >>> nested('jazz.wav') 'audio'- default_factory(sentinel=None)¶
Return a constanc sentinel value when called. Use partial to set sentinel
- key()¶
Return the input unchanged (the default leaf function).
- class i2.routing_forest.RoutingForest(cond_nodes)[source]¶
Bases:
RoutingNode>>> rf = RoutingForest([ ... CondNode(cond=lambda x: isinstance(x, int), ... then=RoutingForest([ ... CondNode(cond=lambda x: int(x) >= 10, then=FinalNode('More than a digit')), ... CondNode(cond=lambda x: (int(x) % 2) == 1, then=FinalNode("That's odd!"))]) ... ), ... CondNode(cond=lambda x: isinstance(x, (int, float)), ... then=FinalNode('could be seen as a float')), ... ]) >>> assert list(rf('nothing I can do with that')) == [] >>> assert list(rf(8)) == ['could be seen as a float'] >>> assert list(rf(9)) == ["That's odd!", 'could be seen as a float'] >>> assert list(rf(10)) == ['More than a digit', 'could be seen as a float'] >>> assert list(rf(11)) == ['More than a digit', "That's odd!", 'could be seen as a float']
- class i2.routing_forest.RoutingNode[source]¶
Bases:
objectA RoutingNode instance needs to be callable on a single object, yielding an iterable or a final value
- class i2.routing_forest.SwitchCaseNode(switch, cases, default=<i2.routing_forest.NoDefault object>)[source]¶
Bases:
RoutingNodeA RoutingNode that implements the switch/case/else logic. It’s just a specialization (enhanced with a “default” option) of the FeatCondNode class to a situation where the cond function of feat_cond_thens is equality, therefore the routing can be implemented with a {value_to_compare_to_feature: then_node} map.
- Parameters:
switch (
Callable) – A function returning the feature of an object we want to switch oncases (
Mapping) – The mapping from feature to RoutingNode that should be yield for that feature. It is often a dict, but only requirement is that it implements thecases.get(val, default)method.default (
Any) – Default RoutingNode to yield if no
>>> rf = RoutingForest([ ... SwitchCaseNode(switch=lambda x: x % 5, ... cases={0: FinalNode('zero_mod_5'), 1: FinalNode('one_mod_5')}, ... default=FinalNode('default_mod_5')), ... SwitchCaseNode(switch=lambda x: x % 2, ... cases={0: FinalNode('even'), 1: FinalNode('odd')}, ... default=FinalNode('that is not an int')), ... ]) >>> >>> assert(list(rf(5)) == ['zero_mod_5', 'odd']) >>> assert(list(rf(6)) == ['one_mod_5', 'even']) >>> assert(list(rf(7)) == ['default_mod_5', 'odd']) >>> assert(list(rf(8)) == ['default_mod_5', 'even']) >>> assert(list(rf(10)) == ['zero_mod_5', 'even'])
- i2.routing_forest.return_sentinel(obj, sentinel=None)[source]¶
Return a constanc sentinel value when called. Use partial to set sentinel