Tree data-structures are processed recursively.
In other terms, they represent, or are defined by,
the run of recursive processes. The structure is simple:
class Tree:
def __init__(self, name, *children):
self.name = name
self.children = children
def __iter__(self):
yield from self.children
def __repr__(self):
return self.name
tree = Tree("n", Tree("n1"),
Tree("n2", Tree("n2a"), Tree("n2b")),
Tree("n3", Tree("n3a")))
This walk function provides a recursive generator
of the contents extracted from tree.
NB - it also yields intermediate nodes,
for a flatten, yield tree must
filter out intermediates nodes.
def walk(tree):
yield tree
for _ in tree: yield from walk(_)
print(*walk(tree), sep=', ')
# n, n1, n2, n2a, n2b, n3, n3a
It works on any tree where tree is an Iterator
of sub-Iterators.
The tree must have leaves which are empty
iterators. Examples with list/set/tuple,
and then a tree implemented as a generator.
Useful walks implement stopping conditions,
to support walking infinite trees,
or efficiently run by pruning early.
def prune(tree, maxdepth, *, d=0):
if d >= maxdepth: return
yield d, tree
for _ in tree:
yield from prune(_, maxdepth, d=d+1)
print(*prune(tree, 2), sep=', ')
# n n1 n3 n3a
import os
def walk_fs(dir):
if type(dir) == os.DirEntry:
# prune hidden files, git
if dir.name[0] == '.': return
# prune __pycache__ etc
if dir.name[0] == '_': return
# prune node_modules
if dir.name == "node_modules": return
for _ in os.scandir(dir):
yield _
if _.is_file(): yield _
if _.is_dir(): yield from walk_fs(_)
for file in walk_fs('..'):
if file.name.endswith('.html'):
print('%s ;'*11 % (file.name, *file.stat()))
Communicating with Generators
Generators have a two-way communication channel
using resp = yield, that gets a
value from the outer scope that provides it via
generator.send(...).
Before returning, a last yield
can be used to avoid having to catch the
StopIteration.
def walk_sent(tree):
sentinel = yield tree
if sentinel == "Prune":
yield; return
for _ in tree:
yield from walk_sent(_)
def cowalk_sent(tree):
for node in (g := walk_sent(tree)):
if str(node) == "n2":
g.send("Prune"); continue
yield node
print(*cowalk_sent(tree))
# n n1 n3 n3a (skips the n2 subtree)
This can be useful for defering
the pruning logic to the outside iteration,
but also to let it provide contextual
continuation mechanisms, e.g. provide compatible
Parsers or let it decide the next set of children.