Traversing Trees: Eager vs Lazy Approaches
Tree structures are everywhere โ from file systems and HTML DOMs to nested comment threads and syntax trees. Traversing them efficiently and clearly is a common but often overlooked challenge.
In this article, we'll compare eager and lazy approaches to tree traversal in Python. Specifically, we'll explore:
- What generator-based traversal looks like
- How it compares to traditional list-returning recursion
- Why the lazy approach might be more powerful than it first appears
๐งฐ Tree Structure Setupโ
Let's use a basic tree class with value
and children
:
class Node:
def __init__(self, value, children=None):
self.value = value
self.children = children or []
Now let's create a simple tree:
root = Node("A", [
Node("B", [Node("D"), Node("E")]),
Node("C")
])
Visualized:
A
/ \
B C
/ \
D E
โ Version 1: Eager Traversal (Return a List)โ
def traverse_to_list(node):
result = [node.value]
for child in node.children:
result.extend(traverse_to_list(child))
return result
Usage:
for val in traverse_to_list(root):
print(val)
Output:
A
B
D
E
C
This version accumulates the full result list up front before returning anything.
โจ Version 2: Lazy Traversal (Using a Generator)โ
def traverse(node):
yield node.value
for child in node.children:
yield from traverse(child)
Usage:
for val in traverse(root):
print(val)
Output is identical:
A
B
D
E
C
But behavior is different:
- Values are yielded one-by-one
- No list is created
- Memory usage is minimal
๐ When to Use Eachโ
Situation | Eager List | Lazy Generator |
---|---|---|
Small trees | โ Simple and fast | โ Also fine |
Large trees | โ Memory-heavy | โ Better performance |
Want full result | โ Easy to get | โ Must convert to list |
Filter/map mid-traversal | โ Requires extra steps | โ Natural with generator expressions |
Streaming use case | โ Not viable | โ Ideal |
๐งช Bonus: Filtering While Traversingโ
Because generators yield values lazily, you can filter on the fly:
for val in traverse(root):
if val.startswith("D"):
print("Found:", val)
Or use a generator expression:
found = (v for v in traverse(root) if v.startswith("D"))
print(next(found)) # Found: D
๐ What's Next?โ
In real-world scenarios, you usually start with nested dictionaries, flat lists, or JSON โ not pre-built Node
trees. Check out this article for more on building trees from raw data
๐ TL;DRโ
- Generator-based tree traversal is clean, elegant, and memory-efficient.
- Recursive list-returning traversal is simple but can become inefficient.
- Use generators when you want laziness, streaming, or in-place filtering.
Both techniques have their place โ the key is knowing which one matches your use case.