Guillaume Lemaître
Guillaume Lemaître

Reputation: 1280

for-based syntax to skip call to 'iter'

Considering the following iterator:

class MyIterator:

  def __iter__(self):
    print("Calling `__iter__`")
    return self

  def __reversed__(self):
    print("Calling `__reversed__`")
    return self

  def __next__(self):
    print("Calling `__next__`")
    raise StopIteration

Is there a for-based equivalent syntax of the following code:

R = reversed(MyIterator())
while True:
  try:
    next(R)
  except StopIteration:
    break

which actually prints

Calling `__reversed__`
Calling `__next__`

for syntax seems to always add a call to iter, even though the given object already respects the Iterator protocol.

Upvotes: 0

Views: 38

Answers (1)

ShadowRanger
ShadowRanger

Reputation: 155506

Calling __iter__ implicitly is intentional; for doesn't know if it's been handed an iterator or an iterable, and needs to unconditionally ensure it has an iterator, which it does by invoking __iter__. If your object is already an iterator, __iter__ is supposed to be idempotent, doing nothing but returning itself (the body should be nothing but return self). There are some tests in existing Python code that detect iterators by testing iter(x) is x, and if you violate the rules by doing something other than returning self in your custom iterator, you're responsible for the misbehavior.

Point is, what you want to do makes no sense. Either write an iterable (without a __next__, and a __iter__ that returns a new iterator) or write an iterator, but if you write an iterator, you need to obey the rules for iterators.

Just to be clear, the unavoidable order of operations here is:

  1. reversed invokes __reversed__, which should in theory return a new reversed iterator (it's very weird to have it return the existing, theoretically forward iterator)
  2. for invokes __iter__ implicitly on whatever reversed returns to ensure it's an iterator (if __reversed__ were implemented properly, it would have returned a brand new iterator, but instead, you end up invoking __iter__ on the same object)
  3. for then call __next__ implicitly for each item until StopIteration is raised

To fix it, have __reversed__ return a new reverse iterator, not self; the new iterator's __iter__ will still be invoked, but it won't be the one on the original iterator. Even better, split your implementations; something that's reversible shouldn't be an iterator in the first place. Just make __iter__ and __reversed__ both generator functions that produce whatever they should produce, and get rid of __next__, to make your class iterable and reversible, but not an iterator.

Upvotes: 3

Related Questions