Skip to main content

rosetree

PyPI - Version GitHub Actions Workflow Status Coverage Status License: MIT pre-commit

Installation

pip install rosetree

Or, to ensure you have all the required dependencies for tree drawing:

pip install rosetree[draw]

Basics

rosetree provides a generic multi-way tree ("rose tree") data structure. It is inspired by Haskell's Data.Tree library and is well suited to programming in a functional style.

We'll proceed by way of examples. First, let's create a simple Tree with integer node labels:

from rosetree import Tree

# build the tree
>>> tree = Tree(1, [Tree(2, [Tree(3), Tree(4)]), Tree(5)])

# draw the tree
>>> print(tree.pretty())
    1
  ┌─┴──┐
  2    5
 ┌┴─┐
 3  4

We construct a Tree by specifying its root node and a list of child subtrees. A leaf node is therefore represented simply by a Tree with a node and no children.

Drawing

The pretty method of a Tree produces an "ASCII art" string that can be used to view the tree's structure. There are three styles you can choose from by providing a style argument to pretty:

# (this is the default style)
>>> print(tree.pretty(style='top-down'))
    1
  ┌─┴──┐
  2    5
 ┌┴─┐
 3  4

>>> print(tree.pretty(style='bottom-up'))
    1
  ┌─┴──┐
  2    │
 ┌┴─┐  │
 3  4  5

>>> print(tree.pretty(style='long'))
1
├── 2
│   ├── 3
│   └── 4
└── 5

Alternatively you can create an image representation of the tree (this requires matplotlib):

# pop up a GUI window
>>> tree.draw()

# or just save a file directly
>>> tree.draw('my_tree.png')
Picture of a Tree produced by Tree.draw

You may also provide a draw_options argument to customize some aspects of the tree drawing such as node color, edge color, text font, spacing, etc.

Accessing elements and properties

A Tree is actually implemented as a simple Python list of child subtrees, plus an additional node attribute to access the top-level node:

# this is the root node
>>> tree.node
1

# this is the left-hand child subtree
>>> tree[0]
Tree(2, [Tree(3, []), Tree(4, [])])

# this is the right subtree of the left subtree
>>> tree[0][1]
Tree(4, [])

# this is the node at that subtree
>>> tree[0][1].node
4

# this is the number of child subtrees of the root node
>>> len(tree)
2

A few other Tree methods can be used to calculate properties of the tree:

# total number of nodes in the tree
# NOTE: this is *not* the same as len(tree)!
>>> tree.size
5

# maximum distance from the root to any leaf
>>> tree.height
2

You can iterate over nodes, leaves, edges, or subtrees:

# iterate nodes with "pre-order" traversal (parents before children)
>>> list(tree.iter_nodes())
[1, 2, 3, 4, 5]

# iterate nodes with "post-order" traversal (children before parents)
>>> list(tree.iter_nodes(preorder=False))
[3, 4, 2, 5, 1]

# iterate leaves in left-to-right order
>>> list(tree.iter_leaves())
[3, 4, 5]

# iterate edges with pre-order traversal
>>> list(tree.iter_edges())
[(1, 2), (2, 3), (2, 4), (1, 5)]

# iterate all subtrees with pre-order traversal
>>> list(tree.iter_subtrees())
[Tree(1, [Tree(2, [Tree(3, []), Tree(4, [])]), Tree(5, [])]),
 Tree(2, [Tree(3, []), Tree(4, [])]),
 Tree(3, []),
 Tree(4, []),
 Tree(5, [])]

Note: all of these iter_ methods produce a lazy generator to avoid storing all the items in memory. You can step through the generator with a for loop, or generate all the elements by calling list, as in the examples above.

For convenience, you can call tree.leaves to get a list of all the leaf nodes.

Inserting and deleting elements

A Tree, being a Python list, is capable of inserting or deleting elements fairly easily.

from copy import deepcopy

# make a copy of the tree, since we'll be modifying it
>>> tree_copy = deepcopy(tree)
>>> print(tree_copy.pretty())
    1
  ┌─┴──┐
  2    5
 ┌┴─┐
 3  4

# insert another child node below the root
>>> tree_copy.insert(1, Tree(6))
>>> print(tree_copy.pretty())
      1
  ┌───┴┬──┐
  2    6  5
 ┌┴─┐
 3  4

# delete node 4
>>> del tree_copy[0][1]
>>> print(tree_copy.pretty())
    1
 ┌──┼──┐
 2  6  5
 │
 3

Note: Python lists are not optimized for insertion/deletion performance, so you should use this approach sparingly. It is better to build your tree structure up-front and change it as little as possible.

Functional tree operations

Tree exposes a variety of operations from functional programming, which allow you to manipulate trees in a consistent way regardless of the kind of data they contain. We'll go through a few examples.

Map: apply a function element-wise

The map method takes a function and applies it to each element of the tree, preserving the tree structure.

def square(x):
    """Take the square of a number."""
    return x ** 2

# square the value of each node
>>> tree_squared = tree.map(square)
>>> print(tree_squared.pretty())
     1
  ┌──┴──┐
  4     25
 ┌┴─┐
 9  16

# equivalently, as shorthand we can use a lambda expression
>>> tree.map(lambda x: x ** 2) == tree_squared
True

For convenience there is also a leaf_map method, which applies the function only to leaf nodes, and an internal_map method, which applies the function only to internal (non-leaf) nodes.

>>> print(tree.leaf_map(square).pretty())
     1
  ┌──┴──┐
  2     25
 ┌┴─┐
 9  16

>>> print(tree.internal_map(square).pretty())
    1
  ┌─┴──┐
  4    5
 ┌┴─┐
 3  4

Finally, the bimap method applies two different functions to the internal nodes and leaf nodes:

>>> print(tree.bimap(lambda x: x * 100, square))
    100
  ┌──┴──┐
 200    25
 ┌┴─┐
 9  16

Reduce: combine all nodes together

The reduce method takes a function with two arguments used to combine two values into a single value. It recursively applies the operation to combine all nodes into one value.

The input function should take two values of the same type and return a value of that type. The function does not have to be associative or commutative, but reduce will be more "well-behaved" if it is (i.e. the result will not depend on the arrangement of nodes in the tree).

from operator import add, mul

# add all the nodes together
>>> tree.reduce(add)
15

# multiply all the nodes together
>>> tree.reduce(mul)
120

# concatenate all the nodes together (as strings)
# NOTE: this operation is associative but *not* commutative, so order matters
>>> tree.map(str).reduce(add)
'12345'

Scan: partial reduce over each subtree

The scan method performs an operation which replaces each node in the tree with the result of running reduce with some function over that node's subtree. In effect this creates a tree of "partial" reductions.

For instance, if the provided function is add, this will produce the tree of partial sums.

from operator import add

# original tree
>>> print(tree.pretty())
    1
  ┌─┴──┐
  2    5
 ┌┴─┐
 3  4

# tree of partial sums
>>> print(tree.scan(add).pretty())
    15
  ┌─┴──┐
  9    5
 ┌┴─┐
 3  4

Fold: general purpose bottom-up recursion

The fold method is a general purpose construct for performing bottom-up recursion on a tree. The input is a function f taking two arguments, a parent node and a list of already-processed children. Starting at the root node, tree.fold(f) does the following:

  1. Recursively call subtree.fold(f) on each of the child subtrees.
  2. Return the result of f(parent_node, processed_subtrees).

In functional programming this is also known as a tree catamorphism. It captures the pattern of building some value from the bottom of the tree upward. This means that nodes can pass information up to their ancestors, but not vice versa.

Fold is in fact a generalization of all the previous patterns discussed in this section; you can actually express map, reduce, and scan all in terms of fold!

Here is an example of using fold to modify a tree so that each node is converted to a pair (node, num_descendants), where node is the original node's value, and num_descendants is the total number of descendants of that node.

def f(node, children):
    # add the number of children to the total number of childrens' descendants
    num_descendants = len(children) + sum(child.node[1] for child in children)
    # return a new tree whose root node includes the number of descendants
    return Tree((node, num_descendants), children)

>>> tree_with_descendants = tree.fold(f)

>>> print(tree_with_descendants.pretty())
           (1, 4)
       ┌─────┴─────┐
     (2, 2)      (5, 0)
   ┌───┴───┐
 (3, 0)  (4, 0)

License

This library is open-source and licensed under the MIT License.

Contributions are welcome!

Metadata

Release files for rosetree 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for rosetree 0.3.0
File Size Uploaded
rosetree-0.3.0.tar.gz 22.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for rosetree 0.3.0
File Interpreter ABI Platform
rosetree-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 47.8 kB

Release files / rosetree-0.3.0.tar.gz

Download URL rosetree-0.3.0.tar.gz
Size 22.6 kB
Tags Source
SHA-256 checksum
How to use checksums
192a871951edc2ad60aece936779398ed601a5004887744ff44d039731f86647
BLAKE2b-256 checksum
How to use checksums
6236f3be02f60561b32f6f0b95e63a30c324ca7d628c0524d9ff0e39933b1f3b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via Hatch/1.16.5 cpython/3.13.11 HTTPX/0.28.1

Release files / rosetree-0.3.0-py3-none-any.whl

Download URL rosetree-0.3.0-py3-none-any.whl
Size 25.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ac31587eefd4161825447bea4fa61a8d440fa8433290cb4bb7db533bc3c9a984
BLAKE2b-256 checksum
How to use checksums
e3decd2360c7585e6024103845fe6918b97ff980a5a62187a32bf8582daac985
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via Hatch/1.16.5 cpython/3.13.11 HTTPX/0.28.1

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page