Why does zip eagerly evaluate the elements of its arguments?

Viewed 55

Using the following definitions:

def a(x):
    return x + 1

def b(x):
    return x + 2

[map(a, range(3)), map(b, range(3)] results in [<iterator>, <iterator>]. I am trying to replicate this functionality without having to construct the range iterator twice.

I've made this function:

def map_many(mappers, values):
    def get_many(value):
        for mapper in mappers:
            yield mapper(value)

    mapped_values = map(get_many, values)
    return zip(*mapped_values)

Doing:

for my_iterator in map_many((a, b), range(3)):
    print(my_iterator)

results in:

(1, 2, 3)
(2, 3, 4)

Why is my_iterator eagerly evaluated? I would have expected it to still be an iterator that would be lazily evaluated (that, when printed, would show up as something along the lines of <iterator>). The * before mapped_values, to my understanding, unpacks mapped_values itself (which is an iterators of iterators), rather than unpacking all the iterators that would be returned by continually calling next(mapped_values).

Is it possible to have each my_iterator remain as an iterator and lazily evaluated, rather than being converted to a tuple?

1 Answers

According to the documentation of zip, it's equivalent to:

def zip(*iterables):
    # zip('ABCD', 'xy') --> Ax By
    sentinel = object()
    iterators = [iter(it) for it in iterables]
    while iterators:
        result = []
        for it in iterators:
            elem = next(it, sentinel)
            if elem is sentinel:
                return
            result.append(elem)
        yield tuple(result)

As you can see, it does evaluate each of the iterators it's given, and calls next on each of them until at least one of them is exhausted. From this perspective, what you're seeing is the expected behavior.

Related