Basic functions (sum, len, etc.) with recursively-flattened arguments Based on the excellent answers provided for my previous question, I've thoroughly revised my attempt at "deep" versions of all, any, etc. It uses the recommended encapsulated flattening function, and streamlines some unnecessary stuff.

def flatten(*sequence, preserve=False):
    for item in sequence:
        try:
            assert hasattr(item, '__iter__') and not isinstance(item, str)
            if preserve:
                next(iter(item))
        except (AssertionError, StopIteration):
            yield item
        else:
            for i in item:
                yield from flatten(i, preserve=preserve)

def dsum(*args, preserve=False, s=''):
    if any(isinstance(item, str) for item in flatten(args)):
        return s.join(map(str, flatten(args, preserve=preserve)))
    iter_ = flatten(args, preserve=preserve)
    first = next(iter_)
    return sum(iter_, first)

def dlen(*args, preserve=False, s=False):
    return sum(len(arg) if s and isinstance(arg, str) else 1 for arg in flatten(args, preserve=preserve))


The flatten() function is "public" and intended to be accessed from outside the module. It handles an arbitrary number of arguments and generates each non-iterable element (strings are treated as non-iterable). If the keyword argument preserve is set to True, empty iterables are preserved.

I am not aware of any way to use if to test whether a generator/iterable has any items to yield - is there such a way? If not, is my use of assert an acceptable way of turning the if statements into exceptions, or is it better to use if... raise, or something else entirely?

The dsum() function adds together all the items yielded by flatten(). If preserve is True, empty iterables are preserved. If at least one item is a string, everything is converted into a string and joined, connected by the optional s argument. If there are no strings, all items after the first are added onto the first item. If there aren't any strings and there are items that can't be added, the function fails, which I find acceptable.

The dlen() function finds the total length of all the items yielded by flatten(). If preserve is True, empty iterables are preserved and counted as a length of 1. Strings are counted as objects of length 1 unless the optional keyword argument s is True, in which case their actual lengths (number of characters) are counted.

I have removed dany(), dall(), etc. because they were just wrappers around flatten() with zero additional logic.

Test suite (passes):

from deep import *
import datetime as dt

assert dsum([1,2], 3, '1','2','3','456', []) == '123123456'
assert dsum([1,2], 3, '1','2','3','456', [], preserve=True) == '123123456[]'
assert dsum('a', 'b') == 'ab'
assert dsum(1, [2,[3,[4,[5],[]]]]) == 15
# dsum(1, [2,[3,[4,[5],[]]]], preserve=True) # can't sum int with preserved list
assert all(flatten(1, 1, [1, []]))
assert not all(flatten(1, 1, [1, []], preserve=True))
assert all(flatten(1, 1, set()))
assert not all(flatten(1, 1, set(), preserve=True))
assert list(flatten((1, 1, []))) == [1,1]
assert list(flatten((1, 1, []), preserve=True)) == [1,1,[]]
assert dsum(dt.timedelta(3), dt.timedelta(4), s=dt.timedelta()) == dt.timedelta(7)
That is an inappropriate use of an assertion. You should only assert conditions that you know must be true. Furthermore, you must not rely on assertions for correct execution of your program, since assertions can be disabled.

I would also add that I find the execution flow in flatten() difficult to follow.