coroutines
Billy's accepted answer presents a false trichotomy. There are more than three (3) ways to do this. Coroutines are perfectly suited for this because they are pausable, resumable, and cancellable -
def dfs(t):
if not t:
return # <- stop
else:
yield from dfs(t.left) # <- delegate control to left branch
yield t.value # <- yield a value and pause
yield from dfs(t.right) # <- delegate control to right branch
The caller has control over coroutines execution -
def search(t, query):
for val in dfs(t):
if val == query:
return val # <- return stops dfs as soon as query is matched
return None # <- otherwise return None if dfs is exhausted
Languages that support coroutines typically have a handful of other generic functions that makes them useful in a wide variety of ways
persistent iterators
Another option similar to coroutines is streams, or persistent iterators. See this Q&A for a concrete example.