13

I'm trying to figure out how to make iterator, below is an iterator that works fine.

class DoubleIt:

    def __init__(self):
        self.start = 1

    def __iter__(self):
        self.max = 10
        return self

    def __next__(self):
        if self.start < self.max:
            self.start *= 2
            return self.start
        else:
            raise StopIteration

obj = DoubleIt()
i = iter(obj)
print(next(i))

However, when I try to pass 16 into the second argument in iter() (I expect the iterator will stop when return 16)

i = iter(DoubleIt(), 16)
print(next(i))

It throws TypeError: iter(v, w): v must be callable Therefore, I try to do so.

i = iter(DoubleIt, 16)
print(next(i))

It returns <main.DoubleIt object at 0x7f4dcd4459e8>. Which is not I expected. I checked the website of programiz, https://www.programiz.com/python-programming/methods/built-in/iter Which said that callable object must be passed in the first argument so as to use the second argument, but it doesn't mention can User defined object be passed in it in order to use the second argument.

So my question is, is there a way to do so? Can the second argument be used with the "Self defined Object"?

Stephen Fong
  • 697
  • 5
  • 17
  • great question! if [use of sentinel is encouraged](https://amir.rachum.com/blog/2013/11/10/python-tips-iterate-with-a-sentinel-value/), how do I construct one? this may be helpful: https://stackoverflow.com/questions/40297321/what-is-the-2nd-argument-for-the-iter-function-in-python. My reading of this is that \_\_iter\_\_ method shoudl have a second optional argument. – Evgeny Jun 14 '18 at 09:28
  • Possible duplicate of [What is the 2nd argument for the iter function in Python?](https://stackoverflow.com/questions/40297321/what-is-the-2nd-argument-for-the-iter-function-in-python) – quamrana Jun 14 '18 at 09:29
  • 1
    @quamara: the link you supplied above deals with using `iter(x, v)`, but hte question is about making an iterable, it is clearly not duplicate. – Evgeny Jun 14 '18 at 09:39
  • 2
    My question is not duplicate, as what I'm asking is can the second argument be used with the "Self defined Object". That's different. @quamrana – Stephen Fong Jun 14 '18 at 09:40

2 Answers2

14

The documentation could be a bit clearer on this, it only states

iter(object[, sentinel])

...

The iterator created in this case will call object with no arguments for each call to its __next__() method; if the value returned is equal to sentinel, StopIteration will be raised, otherwise the value will be returned.

What is maybe not said perfectly clearly is that what the iterator yields is whatever the callable returns. And since your callable is a class (with no arguments), it returns a new instance of the class every iteration.

One way around this is to make your class callable and delegate it to the __next__ method:

class DoubleIt:

    def __init__(self):
        self.start = 1

    def __iter__(self):
        return self

    def __next__(self):
        self.start *= 2
        return self.start

    __call__ = __next__

i = iter(DoubleIt(), 16)
print(next(i))
# 2
print(list(i))
# [4, 8]

This has the dis-/advantage that it is an infinite generator that is only stopped by the sentinel value of iter.

Another way is to make the maximum an argument of the class:

class DoubleIt:

    def __init__(self, max=10):
        self.start = 1
        self.max = max

    def __iter__(self):
        return self

    def __next__(self):
        if self.start < self.max:
            self.start *= 2
            return self.start
        else:
            raise StopIteration

i = iter(DoubleIt(max=16))
print(next(i))
# 2
print(list(i))
# [4, 8, 16]

One difference to note is that iter stops when it encounters the sentinel value (and does not yield the item), whereas this second way uses <, instead of <= comparison (like your code) and will thus yield the maximum item.

Community
  • 1
  • 1
Graipher
  • 6,891
  • 27
  • 47
  • 3
    `__call__ = __next__` is super ingenious! – Evgeny Jun 14 '18 at 09:54
  • Is it more appropriate to say "What is not explicitly said is that what the iterator yields is whatever the OBJECT returns."? As in my very top code, I pass an obj of the class, which is not callable into iter() – Stephen Fong Jun 14 '18 at 09:57
  • Nice explanation but it might be worth adding a sentence or two pointing out that, generally speaking, objects which support `iter` typically only support either one form or the other. Because the two protocols are so different. – strubbly Jun 14 '18 at 10:07
  • @strubbly Which two protocal are you mentioning? Iterator protocal and ...? – Stephen Fong Jun 14 '18 at 10:09
  • The documentation does state that non-sentinel values will be returned: "The iterator created in this case will call `object` with no arguments for each call to its `__next__()` method; if the value returned is equal to `sentinel`, `StopIteration` will be raised, otherwise the value will be returned." The first "returned" refers to the callable (first argument to iter), the second to what is returned by `__next__`. – Yann Vernier Jun 14 '18 at 10:15
  • @StephenFong The protocol nicely quoted in Yann Vernier's subsequent comment :-) – strubbly Jun 14 '18 at 10:19
  • That's an alternate mode for the [`iter`](https://docs.python.org/3/library/functions.html#iter) builtin function, not the [`__iter__`](https://docs.python.org/3/reference/datamodel.html#object.__iter__) special method. Both produce iterators, the latter is the primary way to implement the iterable protocol, and the iterator implements the iterator protocol (having `__next__` as well as `__iter__` returning self). – Yann Vernier Jun 14 '18 at 10:26
3

Here's an example of a doubler routine that would work with the two argument mode of iter:

count = 1
def nextcount():
    global count
    count *= 2
    return count

print(list(iter(nextcount, 16)))
# Produces [2, 4, 8]

This mode involves iter creating the iterator for us. Note that we need to reset count before it can work again; it only works given a callable (such as a function or bound method) that has side effects (changing the counter), and the iterator will only stop upon encountering exactly the sentinel value.

Your DoubleIt class provided no particular protocol for setting a max value, and iter doesn't expect or use any such protocol either. The alternate mode of iter creates an iterator from a callable and a sentinel value, quite independent of the iterable or iterator protocols.

The behaviour you expected is more akin to what itertools.takewhile or itertools.islice do, manipulating one iterator to create another.

Another way to make an iterable object is to implement the sequence protocol:

class DoubleSeq:
    def __init__(self, steps):
        self.steps = steps
    def __len__(self):
        return self.steps
    def __getitem__(self, iteration):
        if iteration >= self.steps:
            raise IndexError()
        return 2**iteration

print(list(iter(DoubleSeq(4))))
# Produces [1, 2, 4, 8]

Note that DoubleSeq isn't an iterator at all; iter created one for us using the sequence protocol. DoubleSeq doesn't hold the iteration counter, the iterator does.

Yann Vernier
  • 15,414
  • 2
  • 28
  • 26