How can we unpack an arbitrarily nested iterator, of iterators, of [...], of iterators?

Viewed 119

If we have an iterator of non-iterators, then we can unroll (unpack) it as follows:

unroll = lambda callable, it: callable(it)

inputs = range(0, 10)
print(unroll(list, inputs))
# prints "[1, 2, 3, 4, 5, 6, 7, 8, 9]"

If we have an iterator of iterators or non-iterators, then we can unroll it as follows:

unroll = lambda callable, it: callable(map(callable, it))

inputs = [range(0, 2), range(2, 4), range(4, 6)]
print(unroll(list, inputs))
# prints "[[0, 1], [2, 3], [4, 5]]"

I don't want to flatten the iterator. A flattening of [[0, 1], [2, 3], [4, 5]] is [0, 1, 2, 3, 4, 5] I want to preserve nesting, but have fully-populated containers (lists, tuples, arrays, etc...) instead of iterators.

The question is, how can we unroll an iterator of arbitrarily nested iterators? My attempt is shown below, but it doesn't work.

import abc
class StringPreservationistBase(abc.ABC):
    @abc.abstractmethod
    def __str__(i):
        raise NotImplementedError()

class StringPreservationist(StringPreservationistBase):
    """
    The idea behind this class if you get
    something which requires calculation, then
    the result is stored for future read-like
    operations until such a time that the value
    becomes stale.

    For example, if this was a `Square` class
    `Square.get_area()` would only compute `length*width`
    the first time.
    After that, `Square.get_area()` would simply returned
    the pre-calculated value stored in `area`.

    If any member variable which `Square.getarea()`
    reads from are written to, then the process resets.

    That is, if `length` or `width` were written to,
    then we go back to the implementation of
    `Square.getarea()` which calculates `length*width`

    For this particular class the result of
    `__str__` is stored.
    """

    # Any method with write permission
    # is supposed to set state back to StringPreservationistState0
    #
    # That is, if string become stale, we
    # delete the string
    #
    def __init__(i, elem, count: int):
        i._count = count
        i._elem = elem
        i._state = i._StringPreservationistState0(i)

    def __len__(i):
        return i._count

    def __iter__(i):
        return itts.repeat(i._elem, i._count)

    def __str__(i):
        stryng = str(i._state)
        i._state = i._StringPreservationistState1(i, stryng)
        return stryng

    class _StringPreservationistState1(StringPreservationistBase):
        def __init__(i, x, stryng: str):
            i._x = x
            i._stryng = stryng

        def __str__(i):
            return i._stryng

    class _StringPreservationistState0(StringPreservationistBase):
        def __init__(i, x):
            i._x = x

        def __str__(i):
            # s = '',join(itts.repeat(i._x._elem, i._x._count))
            s = ''.join(str(x) for x in i._x)
            return s

class Spacer(StringPreservationistBase):
    def __init__(i, count: int):
        i._innerself = StringPreservationist(" ", count)

    def __len__(i):
        return len(i._innerself)

    def __iter__(i):
        return iter(i._innerself)

    def __str__(i):
        return str(i._innerself)
# end class

def indent_print(parent, indent=Spacer(0)):
    assert(not isinstance(parent, type("")))
    # "a"[0][0][0][0][0][0] == "a"[0]
    try:
        for child in parent:
            nxt_indent = type(indent)(4 + len(indent))
            indent_print(child, nxt_indent)
    except: # container not iterable
        print(indent, parent)

# def get_indent_iter(parent, indent=Spacer(0)):
#     try:
#         for child in parent:
#             it = indent_print(child, type(indent)(4 + len(indent)))
#             yield something
#     except: # container not iterable
#         yield indent
#         yield parent

def rasterize_dot_verify_args(callable, parent):
    if not hasattr(callable, "__call__"):
        raise ValueError()
    import inspect
    siggy = inspect.signature(callable)
    if (len(siggy.parameters) > 1):
        raise ValueError()

def rasterize(callable, xparent, make_copy:bool = False):
    rasterize_dot_verify_args(callable, xparent)

    iparent = xparent
    if make_copy:
        import copy
        iparent = copy.deepcopy(xparent)

    if hasattr(iparent, "__iter__"):
        iter_kids = iter(iparent)
        if iter_kids != iparent:
            # ----------------------------------
            # why
            #    iter_kids != parent
            # ?!???
            # ----------------------------------
            # because a single character string
            # returns an iterator to iti.
            #
            #     "a"[0][0][0][0][0][0][0][0] = a[0]
            #     iter(iter(iter(iter("a")))) == iter("a")
            #

            lamby = lambda p, *, c=callable: rasterize(c, p)
            out_kids = map(lamby, iter_kids)
            r = callable(out_kids)
        else: # iter_kids == iparent
            r = callable(iter_kids)
    else: # `parent` is not iterable
        r = iparent
    return r

# iterator to non-iterables
# [1, 2, 3, 4]
input0 = "iter([1, 2, 3, 4])"

# iterator to iterators of non-iterables
import itertools as itts
input1A = "map(lambda x: itts.repeat(x, 6), range(1, 5))"
input1B = "iter([range(0, 2), range(1, 3), range(2, 4)])"
# input1A = [
#     [1, 1, 1, 1, 1, 1]
#     [2, 2, 2, 2, 2, 2]
#     ...
#     [2, 2, 2, 2, 2, 2]
# ]
# input1B = [
#     [0, 1]
#     [1, 2]
#     [2, 3]
# ]

inputs = [input0, input1A, input1B]

import copy
for input in inputs:
    print(256 * "#")
    print(input)
    print(list)
    iterator = eval(input)
    raster = rasterize(list, input)
    indent_print(raster)
    print(256*"#")
1 Answers

You can try the following function:

def is_iter(i):
    if isinstance(i, str):
        return len(i) != 1
    return hasattr(i, '__iter__')

def unroll(func, iterator):
    def _unroll(it): # recursive helper method
        if not is_iter(it):
            yield it
        for i in it:
            if not is_iter(i):
                yield i
            else:
                yield func(_unroll(i)) # apply function to iterator and recurse
    return func(_unroll(iterator)) # apply function to end result

>>> inputs = [(0,3), '345']
>>> print(unroll(list, inputs))
[[0, 3], ['3', '4', '5']]
>>> inputs = [range(0, 2), range(2, 4), range(4, 6)]
>>> print(unroll(tuple, inputs))
((0, 1), (2, 3), (4, 5))
>>> print(unroll(list, inputs))
[[0, 1], [2, 3], [4, 5]]
>>> inputs = [[range(0, 2), range(2, 4)], range(4, 6)]
>>> print(unroll(tuple, inputs))
(((0, 1), (2, 3)), (4, 5))
>>> print(unroll(list, inputs))
[[[0, 1], [2, 3]], [4, 5]]
Related