Class with iter but not next

ぃ、小莉子 提交于 2020-03-16 07:54:52

问题


For the iterator protocol you create both an __iter__ and __next__ method. However, what about the following case:

class Item:
    def __init__(self):
        self.name = 'James'
    def __iter__(self):
        return self

Now I can do:

>>> i=Item()
>>> iter(i)
<__main__.Item instance at 0x10bfe6e18>

But not:

>>> next(i)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: instance has no next() method

As far as I'm aware, the definition of iterator/iterable is:

  • Iterable has the method __iter__
  • Iterator has the method __next__

Would this then mean that my item above is an Iterable but not an Iterator? Or would it be neither because doing the following wouldn't work:

>>> for item in i:
...     print (item)
... 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: instance has no next() method

Note this would be the full class that has the iterator methods defined:

class Item:
    def __init__(self):
        self.name = 'James'
        self.i = 0
    def __iter__(self):
        return self
    def __next__(self):
        if self.i >= len(self.name): raise StopIteration
        value = self.name[self.i]
        self.i += 1
        return value

回答1:


You're mostly right with your definitions, but not quite.

  • An object is iterable if it has an __iter__ method that returns an iterator.
  • An object is an iterator if it has a __next__ method to get the next value while iterating. But iterators in Python are also expected to be iterable. They should all have an __iter__ method that returns self.

Your first example has an __iter__ method, but because it returns self and the object is not an iterator (since it has no __next__ method), it's not really a valid iterable either.

To make a non-iterator iterable, you need to return some other object that is a valid iterator. One sneaky way to do it is to make __iter__ a generator method (by using yield in its implementation). But if you have some sequence of values to return, you could also just return an iterator over that sequence.

The class in your last code block is indeed an iterator. But if you wanted to make it an iterable that is not its own iterator (perhaps because you want to be able to iterate over it several times), you would probably want something more like this:

class Item:
    def __init__(self):
        self.name = 'James'
    def __iter__(self):
        return iter(self.name)



回答2:


Would this then mean that my item above is an Iterable but not an Iterator?

No; to be iterable, the __iter__ method should return an iterator. Yours doesn't.

According to the glossary in the official Python docs:

Iterable

An object capable of returning its members one at a time. Examples of iterables include all sequence types (such as list, str, and tuple) and some non-sequence types like dict, file objects, and objects of any classes you define with an __iter__() method or with a __getitem__() method that implements Sequence semantics.

Iterables can be used in a for loop and in many other places where a sequence is needed (zip(), map(), …).

Since your item is not "capable of returning its members one at a time", and cannot "be used in a for loop [or] other places where a sequence is needed", it is not iterable.

Note also that the __next__ method is not sufficient to be an iterator; an iterator must also have an __iter__ method which returns itself:

Iterator objects also need to implement this method; they are required to return themselves.




回答3:


__iter__ and __next__, as iterable and iterator, are different things. And although it is possible to have both methods on the same class, with __iter__ returning self, this would work only for proof of concepts, not for production code.

An iterable will have an __iter__ method that returns an object that has __next__. If both are the same instances as in

this is just a demo, with faulty code


class Item:
   def __init__(self, data):
       self.data = data
   def __iter__(self):
       self.counter = 0
       return self
   def __next__(self):
       self.counter += 1
       if self.counter > len(self.data):
            raise StopIteration()
       return self.data[self.counter - 1]

This will work - but if you try to create two independent iterators on the same instance of Item, they won't work as desired - since both would share the same counter - the attribute counter in the instance.

It is rare however that one needs to implement __next__: if __iter__ is writen as a generator function, having a yield instead of returning self, that will just work. Python will call __next__ on the generator created automatically with each call to __iter__:

this works


class Item:
   def __init__(self, data):
       self.data = data
   def __iter__(self):
       for item in self.data:
            yield item

As you can see, the correct way is "nextless" and much simpler, and can also be implemented, in this case, by returning an independent iterator for the data. (The yield implementation is needed if getting to each item requires some custom computation)

this also works


class Item:
   def __init__(self, data):
       self.data = data
   def __iter__(self):
       return iter(self.data)

(on this case, Python will call __next__ on the iterator created for self.data)

If you really want to implement __next__, the object with that method have to keep track of any counter or pointers needed to retrieve the next items, and that must be independent of the host instance. The most straightforward way to do that is to have a second class, related to your first one, and have __iter__ return an instance of that instead:

working "full" example


class Item:
   def __init__(self, data):
       self.data = data

   def __iter__(self):
       return ItemIterator(self)

class ItemIterator:
   def __init__(self, item):
      self.item = item
      self.counter = 0

   def __next__(self):
       self.counter += 1
       if self.counter > len(self.item.data):
            raise StopIteration()
       return self.item.data[self.counter - 1]



来源:https://stackoverflow.com/questions/60518079/class-with-iter-but-not-next

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!