0

I realised I still don't quite understand how to implement an iterator class. So a class where one can "for-loop" over some of its contents.

I have looked at these answers (I still don't get it):

What exactly are Python's iterator, iterable, and iteration protocols?

Build a Basic Python Iterator

As far as I understand, an iterable is one that implements __iter__ and returns an iterator which is something that has __next__ implemented. From this I somehow understood that if I want my class to be an iterator, and iterable. I must define __iter__ to return self, and have __next__ defined. Am I wrong so far?

Here is a scaffold for my class:

class Wrapper:
    def __init__(self, low, high):
        self.store = OrderedDict()  # it is imported (ommited)

    def __getitem__(self, key):
        return self.store[key]

    def __iter__(self):
        return self
        # Option 3
        return self.store

    def __next__(self):
        # Option 1 
        for key in self.store:
            return key
        # Option 2
        for key in self.store.keys():
            return key

Tried the above options, none worked. :(

What happens is I have some py.tests prepared to test the iteration if it works correctly, just a simple for loop, nothing fancy. And the tests just run forever (or longer then my patience < 5 min), but the mock class has 5 items, so it should not take long.

What am I doing wrong?

SirSteel
  • 123
  • 1
  • 10

2 Answers2

2

You've got your concepts mixed up. You say

I realised I still don't quite understand how to implement an iterator class. So a class where one can "for-loop" over some of its contents.

but that's not how things work. If you just want to be able to perform for loops over instances of your class, you almost certainly shouldn't be making your instances iterators directly. You should write an __iter__ method that returns an iterator, either manually or by yielding, and you should not write a __next__:

# Returning an iterator manually:
class Wrapper:
    ...
    def __iter__(self):
        return iter(self.store)

# or with yield:
class Wrapper:
    ...
    def __iter__(self):
        for thing in self.store:
            yield thing

Iterators are technically iterable, but only once. To write an iterator, you would have __iter__ return self and have __next__ return the next item on each call, or raise StopIteration when the items are exhausted (and continue to raise StopIteration on any further calls). I'll write an iterator over the half-open interval [0, 10), since writing one for an OrderedDict would just delegate to the OrderedDict's iterator for everything too directly to be instructive:

class Iterator:
    def __init__(self):
        self.state = 0
    def __iter__(self):
        return self
    def __next__(self):
        if self.state == 10:
            raise StopIteration
        self.state += 1
        return self.state - 1

Writing iterators manually is unusual even when you do want an iterator, though; it's much more common to use yield:

def iterator():
    for i in range(10):
        yield i

# or managing the index manually, to show what it would be like without range:
def iterator():
    i = 0
    while i < 10:
        yield i
        i += 1
user2357112
  • 260,549
  • 28
  • 431
  • 505
  • Oh, I see! Thank you. I did not really understand those things before. So if all I want is "for-loop" functionality, my `__iter__` returning `iter(self.store)` will suffice. – SirSteel Feb 07 '18 at 18:02
1

You can make the following changes:

class Wrapper:
    def __init__(self, low, high):
        self.store = OrderedDict() 
        self.__iter = None  # maintain state of self as iterator

    def __iter__(self):
        if self.__iter is None:
            self.__iter = iter(self.store)
        return self

    def __next__(self):
        try:
            return next(self.__iter)
        except StopIteration:   # support repeated iteration
            self.__iter = None  
            raise

__next__ should return the next element of an iterator that is being iterated. And your __iter__ method should return self to properly implement the protocol. This ensures for instance, that a call to next during a loop drives forward the same iterator as the loop:

for x in wrapper:
    # do stuff with x
    next(wrapper)  # skip one

For most purposes, impementing __iter__ should suffice:

class Wrapper:
    def __init__(self, low, high):
        self.store = OrderedDict() 

    def __iter__(self):
        return iter(self.store)
user2390182
  • 72,016
  • 6
  • 67
  • 89
  • Thank you, this worked perfectly. It also seems to work without the state maintainance. Could you explain why it is needed? – SirSteel Feb 07 '18 at 17:37
  • 1
    you can always do `for x in wrapper` (calls `__iter__` implicitly), but `next(wrapper)` (calls `__next__` implicitly) should not work properly without maintaining state. – user2390182 Feb 07 '18 at 17:38
  • 1
    Your `__iter__` and `__next__` aren't consistent; you seem to want `Wrapper` instances to be iterators, judging by the presence of `__next__`, but you're not returning `self` from `__iter__`. – user2357112 Feb 07 '18 at 17:40
  • Yes, this does not correctly implement the iterator protocol. Since this class defines an iterator, `__next__` must return `self`. Likely, the OP simply should be defining an iterator, and rather, it should define an iterable that simply delegates to the internal dict as you've done here with no `__next__` method – juanpa.arrivillaga Feb 07 '18 at 17:42
  • @user2357112 What do you mean exactly? Yeah my class definition was wrong, but the schwobaseggl solution seemed to make it work perfectly. – SirSteel Feb 07 '18 at 17:43
  • Not really sure what you all mean now. My `__iter__` would return the `iter(self.store)` and my `__next__` should return self? Why? – SirSteel Feb 07 '18 at 17:46
  • @user2357112 True, I edited my answer to implement the protocol properly. – user2390182 Feb 07 '18 at 17:52
  • @juanpa.arrivillaga You mean `__iter__` should return `self`. True, I edited my answer. – user2390182 Feb 07 '18 at 17:53
  • @schwobaseggl Whoops, yes, `__iter__` should return `self` is what I meant. – juanpa.arrivillaga Feb 07 '18 at 18:18