If you look at your functions, you'll see that they all have almost identical common code:
if len(args) == 1:
try:
iter(args[0])
if type(args[0]) == str or not len(args[0]):
raise TypeError
return A(B(arg) for arg in args[0])
except TypeError:
return C(args[0])
return A(B(arg) for arg in args)
except for the sections I've replaced with A
, B
, and C
. But even though these parts differ from one function to another, B
is always a recursive call to the containing function, A
implements the "combining" logic, and C
computes the result for a single item.
So you could simplify your code by extracting this common code into a function, for example like this:
def map_reduce_tree(f, r, *args):
"""Apply f to each leaf element of the tree args and combine the
results by calling r.
"""
if len(args) == 1:
try:
iter(args[0])
if type(args[0]) == str or not len(args[0]):
raise TypeError
return r(map_reduce_tree(f, r, a) for a in args[0])
except TypeError:
return f(args[0])
else:
return r(map_reduce_tree(f, r, a) for a in args)
Now dall
becomes:
map_reduce_tree(bool, all, *args)
and dany
becomes:
map_reduce_tree(bool, any, *args)
and dsum
becomes:
identity = lambda x:x
map_reduce_tree(identity, sum, *args)
and djoin
becomes:
map_reduce_tree(identity, ''.join, *args)
and so on. In case you're wondering why I've called this map_reduce_tree
, it's because "map–reduce" is a well-known data processing model, and a tree is the recursive data structure that we're operating on.
Now, we can simplify map_reduce_tree
as follows:
Instead of calling iter
and catching a TypeError
if the value doesn't support the iteration interface, we could use the abstract base class collections.abc.Iterable
and write isinstance(x, Iterable)
.
Combine the two instances of r(map_reduce_tree(f, r, a) for a in ...)
into one.
Omit the test not len(args[0])
— it's better for r
to handle an empty sequence of arguments, then for f
to try to handle it.
That results in:
from collections.abc import Iterable
def map_reduce_tree(f, r, args):
"""Apply f to each leaf element of the tree args and combine the
results by calling r.
"""
if isinstance(args, Iterable) and type(args) != str:
return r(map_reduce_tree(f, r, a) for a in args)
else:
return f(args)
But we can decompose the functionality even further. There are really three steps here: (i) walking over the tree recursively finding the leaves, (ii) applying f
to each leaf element; (iii) combining the results by calling r
. So we can split that into three parts, using the leaves
function below for step (i), using the built-in map
for step (ii), and just calling r
for step (iii).
def leaves(tree):
"""Generate the leaf elements of tree."""
if isinstance(tree, Iterable) and type(tree) != str:
for t in tree:
yield from leaves(t)
else:
yield tree
Now dall
becomes:
all(map(bool, leaves(args)))
and dany
becomes:
any(map(bool, leaves(args)))
and dsum
becomes:
sum(leaves(args))
(since f
was the identity function we can just omit the mapping step) and djoin
becomes:
''.join(leaves(args))
which I hope you'll agree is a lot shorter and a lot easier to understand than the original.