Help with Flexible arguments and zip() iterator

I was working with some iterators and flexible arguments in Python. And I’m running into this error.

The code here is

a = (1,2,3,4)
b = ('a', 'b', 'c', 'd')
z = zip(a,b)
print(*z)
lval, rval = zip(*z)
print(lval)
print(rval)

I know when * is used before an iterator, it lets us unpack the sequence. To quote an example,

print(*range(4))
# output would be numbers: 0 1 2 3  which is not a list; each value is of type <int>

What I’m trying to do here is that I’m unpack the variable z into its initial given tuples a and b.

If someone could explain what is going wrong here, that’d be a really great help.

When you printed z you consumed the zip object. There is nothing there for the next line to work with.

If you don’t know how many values there are in the sequence then the best way to preserve the values as read-only is to unpack them into a tuple, which is immutable. For write access then unpack into a list.

range() is not an iterator. If it was then we wouldn’t be able to store it as a reusable object,

>>> a = range(10)
>>> b = range(9, -1, -1)
>>> a, b
(range(0, 10), range(9, -1, -1))
>>> *a, *b
(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)
>>> c = *a, *b
>>> c
(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)
>>> 

c would be nothing if range() was an iterator like zip().

1 Like

An iterator is a stack, whereas an iterable is a sequence, by programming definition, but even by mathematical definition if the difference between terms, in the case the index is 1, where for this language, i=0; n=len(s). It’s the index that forms the mathematical sequence. The data can be anything.

Our above code performs a function on both sequences in respective order. Not by their value, but by their position.

We can’t do anything with c except look up values at various positions.

range() is in literal terms, a sequencer. We can create any sequence we like, so long as the arguments are integers.

>>> [ 1 / x for x in c[1:-1] ]
[1.0, 0.5, 0.3333333333333333, 0.25, 0.2, 0.16666666666666666, 0.14285714285714285, 0.125, 0.1111111111111111, 0.1111111111111111, 0.125, 0.14285714285714285, 0.16666666666666666, 0.2, 0.25, 0.3333333333333333, 0.5, 1.0]
>>> 
1 Like
>>> y = zip(a, b)
>>> y
<zip object at 0x00000243E5847940>
>>> dir(y)
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__lt__', '__ne__', '__new__', '__next__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
>>> y.__next__()
(0, 9)
>>> 

We have consumed the bottom layer of the stack.

>>> list(y)
[(1, 8), (2, 7), (3, 6), (4, 5), (5, 4), (6, 3), (7, 2), (8, 1), (9, 0)]
>>> 

Notice the first tuple is gone?

>>> list(y)
[]
>>> 

Notice that now all the tuples are gone because we consumed them above.

That is how iterators behave, and what makes them iterators.

Thank you so much for the detailed explanation with code examples, @mtf.
So, what I gather from the explanation is that zip is an iterator, and range is an iterable, right? I have a couple of more questions if you could help me out clear them!

  1. Is there any way to differentiate between them except by using them and then checking if the object is consumed or not?
  2. Why is zip() still conceptually grouped together with iterators like range(), enumerate(), zip() and filter()?
  3. Are the same rules of iterator and iterables applicable to enumerate(), map() and filter() as well? Which of these are iterators and iterables?
  4. Is there a detailed documentation available to understand them? If yes, could you share the link with me?

Thank you for the help!

1 Like

Consider,

>>> f = filter(lambda x: x % 2, c)
>>> list(f)
[1, 3, 5, 7, 9, 9, 7, 5, 3, 1]
>>> list(f)
[]
>>> e = enumerate(c)
>>> list(e)
[(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 6), (7, 7), (8, 8), (9, 9), (10, 9), (11, 8), (12, 7), (13, 6), (14, 5), (15, 4), (16, 3), (17, 2), (18, 1), (19, 0)]
>>> list(e)
[]
>>> m = map(lambda x: x + 1, c)
>>> list(m)
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
>>> list(m)
[]
>>> 

See how each are consumed? That is what they all have in common.

This subject is one that deserves study. What is the difference between an iterable and an iterator?

1 Like

Functional Programming HOWTO — Python 3.12.0 documentation

The above is a good read and goes into Python iterators in not too much detail, but enough to give us a clearer idea of iterators.

2 Likes

Awesome. This link paints a much clearer picture as to what is actually happening with these iterators. Apart from range(), all the objects can be called an iterable if we can get an iterator for it. And they’re actually a stream of data, so once next() is called on them, that data is consumed and if not saved, the data is lost. This clears out all of my questions! Thank you so much for all the help, @mtf.

1 Like

You’re welcome. Keep coming back to the subject to cement it in your mind. It’s a lot to take in on one go. Play with it until you are real comfortable, like an hour a week just on iterators.

One member has shed some impressive light on this subject in a good many posts: In the search bar above, type iter @ionatan .