3

I am trying to wrap my head around this phrase:

Every iterator is an iterable, but not every iterable is an iterator.

I understand the difference between an Iterator and Iterable, and I understand the last part that "not every iterable is an iterator".

An iterable is a sequence that can be iterated over. An Iterator is an object that is used to iterate over an iterable.

An Iterable produces an Iterator object through the __iter__()

So, how can an Iterator be an Iterable if the Iterator itself is what is used to iterate over something?

Help me make sense of this. Thank you! .

.

UPDATE 1: Found Answer

First, I want to thank all those who gave a response to my question. However, as helpful, informative, and insightful as it all was, it didn't exactly answer my question.

ANSWER & EXPLANATION 1:

First, the use of the term "Iterable" in the phrase "Every Iterator is an Iterable" was not meant to identify an Iterator as an Iterable object like a list, dictionary, or set. It was meant to say that an Iterator also contains an __iter__() method.

Then my next question was: why in the world would an Iterator object have an __iter__() method? The only purpose of the Iterator object is to use its __next__() method.

Then I found the answer to my question in one line:

Why Python Iterator need a dunder iter function

"Because sometimes you're directly given iterators, for example an opened file or a generator function."

We have to remember that a loop first has to call the __iter__() method of the object that it is given in order to receive the Iterator object for iteration.

But, again, there are situations where you are given an Iterator object directly. And in order to use that Iterator object in a loop for iteration, we need to figure out how to bypass the __iter__() method call that the loop automatically performs. So, the Iterator object is packed with its own __iter__() method that returns itself.

Bingo Bango! your in! Loophole activated. The iteration starts as if the loop was dealing with the Iterable object directly. .

.

UPDATE 2: Answered Continued

A fellow user kindly asked for me to complete the interpretation of the phrase above.

ANSWER & EXPLANATION 2:

First part of the phrase:

Every Iterator is an Iteratable...

Interpretation:

Every Iterator has an __iter__() method

Second Part of the phrase

but not every Iterable is an Iterator

Interpretation:

but not every Iterable has a __next__() method

In python, there are objects that have both an __iter__() and a __next__() method. These objects are both an Iterable and an Iterator. In these objects, the __iter__() method returns itself because it is its owns Iterator (of course).

Example:

class MyRange:
  def __init__(self, start, end):
    self.value = start
    self.end = end

  def __iter__(self):
    return self

  def __next__(self):
    if self.value >= self.end:
       raise StopIteration
    current = self.value
    self.value +=1
    return current

Usage:

nums = MyRange(1,10)

for num in nums:
  print(num)

Now...

Objects like list, dictionary, tuple, and set are only Iterables. These objects only have an __iter__() method, and it returns a separate object (Iterator) that has the __next__() method.

....and thats it!

I apologize if my explanations are too simplistic or obvious. My explanations are aimed at beginner programmers (like me) who can't connect the dots so intuitively like others can.

Anyways, I hope this helps someone. Cheers!

Walter M
  • 4,993
  • 5
  • 22
  • 29
  • The original quote is correct: "not every iterable is an iterator". Your paraphrasing is incorrect: "every iterable is not an iterator". The latter implies that no iterable is an iterator, which is not true. In fact, since every iterator is an iterable, this would imply that there are no iterators, which again is not true. – Tom Karzes Aug 04 '22 at 04:43
  • I disagree with the closing of the question. While improperly formatted, it asks about the design choice, and why `__iter__` returns `self`. @Walter is that what you meant? – Bharel Aug 04 '22 at 05:02
  • "An iterable is a sequence that can be iterated over. An Iterator is an object that is used to iterate over an iterable" Not quite accurate. In Python a **sequence** is a type of container, like a `list`, `tuple`, or `range` object that has a `len` can be indexed with `int` from `0` up to `len(sequence) - 1`. An *iterable* is just *anything that can be iterated over*, i.e., with a for-loop, `for x in iterable` without raising an error at the `for x in itereable` part.,i.e. something which will return an iterator when you do `iter(iterable)` – juanpa.arrivillaga Aug 04 '22 at 05:05
  • Iterators themselves are iterable, because you can do `iterator = iter(iterable)` and then do `for x in iterator` as well, although iterators are *stateful*. Once you iterate over an iterator, it is exhausted – juanpa.arrivillaga Aug 04 '22 at 05:08
  • For the reasons @juanpa.arrivillaga gave, it's a design choice and a protocol requirement. Keep in mind it's not always enforced, as technically many iterators can return "123" in their `__iter__` or [omit it altogether](https://docs.python.org/3/glossary.html#term-iterator), but you should play nice so others will be able to play with you :-) – Bharel Aug 04 '22 at 05:17
  • 1
    @Bharel I mean, such implementations are deemed *broken*. You *can* return `"fooo"` from `__len__` or a `[1, 2, 3]` from `__hash__` but that is just broken code. I guess the way I'd put it is that while dunder methods have documented ways in which they should behave, Python often doesn't enforce these things. – juanpa.arrivillaga Aug 04 '22 at 05:18
  • @juanpa.arrivillaga, true, but unlike `len` or `hash` returning an exception, `iter()` and `next()` will output something. An interesting question arises from an iterator that returns another iterator. Let's say a hypothetical iterator over `[(1,2,3),(4,5,6)]` that on a recursive (`iter(iter(list))`) automatically flattens. I deem it broken but technically it is an iterator isn't it? I agree with your addition regarding documentation and enforcing :-) – Bharel Aug 04 '22 at 05:24
  • 1
    @Bharel note, if you define an iterator (or iterable), `class Foo:` with an `def __iter__(self): return "123"` then `iter(Foo())` will raise a `TypeError`, IOW, if `iter(iterable)` encounters an `__iter__` that *doesn't return an iterator* it will error – juanpa.arrivillaga Aug 04 '22 at 05:59

0 Answers0