From 225b83b6b171d361d633f5ac7974b36a62d8525d Mon Sep 17 00:00:00 2001 From: Gregor Reich Date: Tue, 16 Sep 2025 21:19:11 +0200 Subject: [PATCH 1/3] PreOrderIter with recursion unrolling (deque) --- src/anytree/iterators/preorderiter.py | 17 +++++++++++++---- tests/test_iterators.py | 14 ++++++++++---- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/anytree/iterators/preorderiter.py b/src/anytree/iterators/preorderiter.py index b07cf38..dd8a398 100644 --- a/src/anytree/iterators/preorderiter.py +++ b/src/anytree/iterators/preorderiter.py @@ -1,4 +1,5 @@ from .abstractiter import AbstractIter +from collections import deque class PreOrderIter(AbstractIter): @@ -40,11 +41,19 @@ class PreOrderIter(AbstractIter): @staticmethod def _iter(children, filter_, stop, maxlevel): - for child_ in children: + cur_children = deque(children) + descendantmaxlevel = maxlevel + while cur_children: + child_ = cur_children.popleft() if stop(child_): continue if filter_(child_): yield child_ - if not AbstractIter._abort_at_level(2, maxlevel): - descendantmaxlevel = maxlevel - 1 if maxlevel else None - yield from PreOrderIter._iter(child_.children, filter_, stop, descendantmaxlevel) + if not AbstractIter._abort_at_level(2, descendantmaxlevel): + descendantmaxlevel = descendantmaxlevel - 1 if descendantmaxlevel else None + if len(child_.children) == 1: + cur_children.appendleft(child_.children[0]) + continue + else: + yield from PreOrderIter._iter(child_.children, filter_, stop, descendantmaxlevel) + descendantmaxlevel = maxlevel diff --git a/tests/test_iterators.py b/tests/test_iterators.py index 78c8f1e..6678330 100644 --- a/tests/test_iterators.py +++ b/tests/test_iterators.py @@ -22,17 +22,23 @@ def test_preorder(): g = Node("g", parent=f) i = Node("i", parent=g) h = Node("h", parent=i) + k = Node("k", parent=a) + l = Node("l", parent=k) + m = Node("m", parent=e) - eq_(list(PreOrderIter(f)), [f, b, a, d, c, e, g, i, h]) + eq_(list(PreOrderIter(f)), [f, b, a, k, l, d, c, e, m, g, i, h]) eq_(list(PreOrderIter(f, maxlevel=0)), []) + eq_(list(PreOrderIter(f, maxlevel=2)), [f, b, g]) eq_(list(PreOrderIter(f, maxlevel=3)), [f, b, a, d, g, i]) - eq_(list(PreOrderIter(f, filter_=lambda n: n.name not in ("e", "g"))), [f, b, a, d, c, i, h]) - eq_(list(PreOrderIter(f, stop=lambda n: n.name == "d")), [f, b, a, g, i, h]) + eq_(list(PreOrderIter(f, maxlevel=4)), [f, b, a, k, d, c, e, g, i, h]) + eq_(list(PreOrderIter(f, maxlevel=5)), list(PreOrderIter(f))) + eq_(list(PreOrderIter(f, filter_=lambda n: n.name not in ("e", "g"))), [f, b, a, k, l, d, c, m, i, h]) + eq_(list(PreOrderIter(f, stop=lambda n: n.name == "d")), [f, b, a, k, l, g, i, h]) it = PreOrderIter(f) eq_(next(it), f) eq_(next(it), b) - eq_(list(it), [a, d, c, e, g, i, h]) + eq_(list(it), [a, k, l, d, c, e, m, g, i, h]) def test_postorder(): From 1c034d29e38c5283cae6563c7cdb40b32b75b6dd Mon Sep 17 00:00:00 2001 From: Gregor Reich Date: Sun, 21 Sep 2025 13:43:53 +0200 Subject: [PATCH 2/3] partial recursion unrolling with list avoid the setup costs for deque, while avoiding re-allocation of list memory (hence reversed) --- src/anytree/iterators/preorderiter.py | 7 +++---- tests/test_iterators.py | 10 +++++----- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/anytree/iterators/preorderiter.py b/src/anytree/iterators/preorderiter.py index dd8a398..c6e846e 100644 --- a/src/anytree/iterators/preorderiter.py +++ b/src/anytree/iterators/preorderiter.py @@ -1,5 +1,4 @@ from .abstractiter import AbstractIter -from collections import deque class PreOrderIter(AbstractIter): @@ -41,10 +40,10 @@ class PreOrderIter(AbstractIter): @staticmethod def _iter(children, filter_, stop, maxlevel): - cur_children = deque(children) + cur_children = list(reversed(children)) descendantmaxlevel = maxlevel while cur_children: - child_ = cur_children.popleft() + child_ = cur_children.pop() if stop(child_): continue if filter_(child_): @@ -52,7 +51,7 @@ def _iter(children, filter_, stop, maxlevel): if not AbstractIter._abort_at_level(2, descendantmaxlevel): descendantmaxlevel = descendantmaxlevel - 1 if descendantmaxlevel else None if len(child_.children) == 1: - cur_children.appendleft(child_.children[0]) + cur_children.append(child_.children[0]) continue else: yield from PreOrderIter._iter(child_.children, filter_, stop, descendantmaxlevel) diff --git a/tests/test_iterators.py b/tests/test_iterators.py index 6678330..9f210e8 100644 --- a/tests/test_iterators.py +++ b/tests/test_iterators.py @@ -23,22 +23,22 @@ def test_preorder(): i = Node("i", parent=g) h = Node("h", parent=i) k = Node("k", parent=a) - l = Node("l", parent=k) + o = Node("o", parent=k) m = Node("m", parent=e) - eq_(list(PreOrderIter(f)), [f, b, a, k, l, d, c, e, m, g, i, h]) + eq_(list(PreOrderIter(f)), [f, b, a, k, o, d, c, e, m, g, i, h]) eq_(list(PreOrderIter(f, maxlevel=0)), []) eq_(list(PreOrderIter(f, maxlevel=2)), [f, b, g]) eq_(list(PreOrderIter(f, maxlevel=3)), [f, b, a, d, g, i]) eq_(list(PreOrderIter(f, maxlevel=4)), [f, b, a, k, d, c, e, g, i, h]) eq_(list(PreOrderIter(f, maxlevel=5)), list(PreOrderIter(f))) - eq_(list(PreOrderIter(f, filter_=lambda n: n.name not in ("e", "g"))), [f, b, a, k, l, d, c, m, i, h]) - eq_(list(PreOrderIter(f, stop=lambda n: n.name == "d")), [f, b, a, k, l, g, i, h]) + eq_(list(PreOrderIter(f, filter_=lambda n: n.name not in ("e", "g"))), [f, b, a, k, o, d, c, m, i, h]) + eq_(list(PreOrderIter(f, stop=lambda n: n.name == "d")), [f, b, a, k, o, g, i, h]) it = PreOrderIter(f) eq_(next(it), f) eq_(next(it), b) - eq_(list(it), [a, k, l, d, c, e, m, g, i, h]) + eq_(list(it), [a, k, o, d, c, e, m, g, i, h]) def test_postorder(): From 34b38ee3e20aefaaa7256ee9a64e28130c2c938c Mon Sep 17 00:00:00 2001 From: Gregor Reich Date: Sun, 21 Sep 2025 17:19:40 +0200 Subject: [PATCH 3/3] minor changes --- src/anytree/iterators/preorderiter.py | 3 +-- tests/test_iterators.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/anytree/iterators/preorderiter.py b/src/anytree/iterators/preorderiter.py index c6e846e..9e0d2e1 100644 --- a/src/anytree/iterators/preorderiter.py +++ b/src/anytree/iterators/preorderiter.py @@ -53,6 +53,5 @@ def _iter(children, filter_, stop, maxlevel): if len(child_.children) == 1: cur_children.append(child_.children[0]) continue - else: - yield from PreOrderIter._iter(child_.children, filter_, stop, descendantmaxlevel) + yield from PreOrderIter._iter(child_.children, filter_, stop, descendantmaxlevel) descendantmaxlevel = maxlevel diff --git a/tests/test_iterators.py b/tests/test_iterators.py index 9f210e8..22fb0be 100644 --- a/tests/test_iterators.py +++ b/tests/test_iterators.py @@ -31,7 +31,7 @@ def test_preorder(): eq_(list(PreOrderIter(f, maxlevel=2)), [f, b, g]) eq_(list(PreOrderIter(f, maxlevel=3)), [f, b, a, d, g, i]) eq_(list(PreOrderIter(f, maxlevel=4)), [f, b, a, k, d, c, e, g, i, h]) - eq_(list(PreOrderIter(f, maxlevel=5)), list(PreOrderIter(f))) + eq_(list(PreOrderIter(f, maxlevel=f.height + 1)), list(PreOrderIter(f))) eq_(list(PreOrderIter(f, filter_=lambda n: n.name not in ("e", "g"))), [f, b, a, k, o, d, c, m, i, h]) eq_(list(PreOrderIter(f, stop=lambda n: n.name == "d")), [f, b, a, k, o, g, i, h])