1

I am studying python with the book Beginning Python: From Novice to Professional, and I get confused about the section discussing iterators. There is an example in this section:

>>> Class Fibs:
...    def __init__(self):
...        self.a = 0
...        self.b = 1
...    def __next__(self):
...        self.a, self.b = self.b, self.a + self.b
...        return self.a
...    def __iter__(self):
...        return self
...
>>> fibs = Fibs()
>>> for f in fibs:
...     if f > 1000:
...         print(f)
...         break
...
1597

To be honest, I only know that fibs is an object with methods __next__ and __iter__, but have no idea about what happens in each step of the loop. And I made a test:

>>> isinstance(f, Fibs)
False
>>> f is fibs
False
>>> isinstance(f, int)
True
>>> fibs.a
1597

This makes me much more confused! Why the boolean value of f is fibs is False? And why fibs.a become 1597 after the execution of the loop?(Is the method __next__ automatically called in the loop?) Thanks in advance.

1
  • Well obviously it is called, otherwise print(f) wouldn't give you 1597. And you couldn't do f > 1000 if f wasn't an integer. Commented Oct 10, 2013 at 13:31

5 Answers 5

3
 1  fibs = Fibs()
 2  for f in fibs:
 3      if f > 1000:
 4          print(f)
 5          break

Line 1 creates a Fibs() object, calling __init__(). Line 2 calls __iter__(), which returns an iterator object (in this case, just fibs itself). The interpreter will then begin calling the __next__() method repeatedly; it returns self.a, a plain number, which is assigned to the loop variable f (so of course it's not a Fibs() object and certainly not the one named fibs). When that value reaches 1000, the if clause will fire, print the result, and break out of the loop.

Sign up to request clarification or add additional context in comments.

4 Comments

.__iter__() does not create a generator object. It simply returns self; and because self has a __next__ method, it is an iterator. Generators are specialized iterators, but iterators are not necessarily generators.
If .__iter__() was a method that uses a yield expression, then a generator would be returned.
@LeeDanielCrocker Get a little bit clear now but still have a question: is the method __iter__ called only at the beginning of the loop? For example, I have tried to create a list x=[1,2,3] and dir(x) shows no __next__ method but there is __iter__. So at the beginning of for i in x, x.__iter__() is called, returning a iterator that is really being "used" by the for loop. Is my comprehension correct?
@MartijnPieters Thanks for your help. I am clear now.
1

And why fibs.a become 1597 after the execution of the loop?

Well this is because it is going through the Fibonacci sequence and this is the first number over 1000 in the sequence.

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584 ...

Comments

0

On enter to the loop interpreter calls __iter__ method. On every loop step interpreter calls __next__ method of Fibs.

Comments

0
for f in fibs

implicitly calls fibs.__iter__ and binds f to all the objects that it yields, in turn. None of these objects are instances of Fibs, let alone equal to fibs. The whole is (roughly) equivalent to

# boilerplate, implicit in the "for" notation
__iterator = iter(fibs)
while True:
    try:
        f = next(__iterator)
    except StopIteration:
        break

    # your code, with f now bound to what the iterator yielded
    if f > 1000:
        print(f)
        break

(iter and next are the clean ways to call __iter__ and __next__, respectively.)

Comments

0

Yes, the loop automatically calls __next__.

The for loop does this to the given object:

# turn the object into an iterator
iterator = iter(given_object)
while True:
    try:
        # try to get the next value
        next_value = next(iterator)
    except StopIteration
        # for loop is done, run the `else:` block if there is any
        run_for_else_suite()
        break
    else:
        # set the loop target to `next_value`
        assign_next_value(next_value)
        continue = run_loop_body()
        if not continue:
            break

The iter() function calls given_object.__iter__(), and the next() function calls given_object.__next__(); these functions offer some extra functionality and are the proper way of calling into the iterator API.

So, in each loop iteration, f is assigned the value that the Fib.__next__ method returned.

You can also see that Fib is its own iterator; __iter__ returns self. Other types can return a dedicated iterator object; lists do, for example:

>>> iter([])
<listiterator object at 0x129516610>

Returning dedicated iterators lets you create multiple 'views' on an object where each iterator maintains its own position, like nested loops:

lst = [1, 2, 3]
for i in lst:
    for j in lst;
        print (i, j)  # prints (0, 0), (0, 1), (0, 2), (0, 3), (1, 0), etc.

or you can explicitly reuse the iterator:

lst = [1, 2, 3]
lst_iter = iter(lst)
for i in lst_iter:
    for j in lst_iter:
        print (i, j)  # prints (0, 1), (0, 2)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.