Skip to main content
no need for the map
Source Link
Gareth Rees
  • 49.7k
  • 3
  • 129
  • 210
all(map(bool, leaves(args)))

and(we don't need to apply bool to the leaves—all does that already). Similarly, dany becomes:

any(map(bool, leaves(args)))
all(map(bool, leaves(args)))

and dany becomes:

any(map(bool, leaves(args)))
all(leaves(args))

(we don't need to apply bool to the leaves—all does that already). Similarly, dany becomes:

any(leaves(args))
Source Link
Gareth Rees
  • 49.7k
  • 3
  • 129
  • 210

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:

  1. 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).

  2. Combine the two instances of r(map_reduce_tree(f, r, a) for a in ...) into one.

  3. 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.