On Mon, Nov 23, 2020 at 8:20 AM Brian Coleman <brianfcoleman@gmail.com> wrote:
Take as an example a function designed to process a tree of nodes similar to that which might be output by a JSON parser. There are 4 types of node:

- A node representing JSON strings
- A node representing JSON numbers
- A node representing JSON arrays
- A node representing JSON dictionaries

The function transforms a tree of nodes, beginning at the root node, and proceeding recursively through each child node in turn. The result is a Python object, with the following transformation applied to each node type:

- A JSON string `->` Python `str`
- A JSON number `->` Python `float`
- A JSON array `->` Python `list`
- A JSON dictionary `->` Python `dict`

I have implemented this function using 3 different approaches:

- The visitor pattern
- `isinstance` checks against the node type
- Pattern matching

I've always thought that the alternative to a "switch case" construct in Python (and I suppose most OO languages) is subclassing and method overriding. I guess that's what the "visitor pattern" is here, but it seems to be adding a bunch of unnecessary bolierplate.

So: given that you have a special "Node" object anyway, the thing to do is to have those Node object know how to unpack themselves. Then the top "traverse the tree" function becomes a single method or attribute access:

tree = node_tree.value

here's what the Nodes look like in this example:

class Node:
    def __init__(self, val):
        self._value = val

    @property
    def value(self):
        return self._value


class StringNode(Node):
    pass


class NumberNode(Node):
    pass


class ListNode(Node):
    @property
    def value(self):
        return [item.value for item in self._value]


class DictNode(Node):
    @property
    def value(self):
        return {k: item.value for k, item in self._value.items()}


Of course, this requires that you have control of the Node objects, rather than getting them from some other library -- but that seems to be what all the examples here are anyway.

If you do need to parse out a tree of object that are not "special" already, then you need to do some type of pattern matching / isinstance checking. In this case, I wrote a function that builds up a tree of Nodes from arbitrary Python objects:

def make_nodes_from_obj(obj):
    if isinstance(obj, str):
        return StringNode(obj)
    if isinstance(obj, Real):
        return NumberNode(obj)
    if isinstance(obj, Sequence):
        return ListNode([make_nodes_from_obj(item) for item in obj])
    if isinstance(obj, Mapping):
        return DictNode({k: make_nodes_from_obj(item)
                         for k, item in obj.items()})

And that could benefit from pattern matching, I suppose, though it's not very compelling to me.

And in "real world" code, I've done just this -- building a system for saving / restoring dataclasses to/from JSON. In that case, each of the dataclasses knows how to save itself and build itself from JSON-compatible python objects (numbers, dicts, strings, lists) -- so again, no need for pattern matching there either. And what I really like about the approach of putting all the logic in the "nodes" is that I can make new types of nodes without having to touch the code at the "top" that visits those nodes.

In short -- I'm still looking for a more compelling example :-)

-CHB

--

Christopher Barker, Ph.D.
Oceanographer

Emergency Response Division
NOAA/NOS/OR&R            (206) 526-6959   voice
7600 Sand Point Way NE   (206) 526-6329   fax
Seattle, WA  98115       (206) 526-6317   main reception

Chris.Barker@noaa.gov