问题
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 returnsself
.
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
, andtuple
) and some non-sequence types likedict
, 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