SeasonalShot SeasonalShot - 4 months ago 15
Python Question

Equivalent code of __getitem__ in __iter__

I am trying to understand more about

__iter__
in Python 3. For some reason
__getitem__
is better understood by me than
__iter__
. I think I get somehow don't get the corresponding next implemention followed with
__iter__
.

I have this following code:

class Item:
def __getitem__(self,pos):
return range(0,30,10)[pos]

item1= Item()
print (f[1]) # 10
for i in item1:
print (i) # 0 10 20


I understand the code above, but then again how do i write the equivalent code using
__iter__
and
__next__()
?

class Item:
def __iter__(self):
return self
#Lost here
def __next__(self,pos):
#Lost here


I understand when python sees a
__getitem__
method, it tries iterating over that object by calling the method with the integer index starting with
0
.

Jim Jim
Answer

In general, a really good approach is to make __iter__ a generator by yielding values. This might be less intuitive but it is straight-forward; you just yield back the results you want and __next__ is then provided automatically for you:

class Item:
   def __iter__(self):
      for item in range(0, 30, 10):
          yield item

This just uses the power of yield to get the desired effect, when Python calls __iter__ on your object, it expects back an iterator (i.e an object that supports __next__ calls), a generator does just that, producing each item as defined in your generator function (i.e __iter__ in this case) when __next__ is called:

>>> i = iter(Item())    
>>> print(i)  # generator, supports __next__
<generator object __iter__ at 0x7f6aeaf9e6d0>
>>> next(i)
0
>>> next(i)
10
>>> next(i)
20

Now you get the same effect as __getitem__. The difference is that no index is passed in, you have to manually loop through it in order to yield the result:

>>> for i in Item():
...    print(i)    
0
10
20

Apart from this, there's two other alternatives for creating an object that supports Iteration.

One time looping: Make item an iterator

Make Item an iterator by defining __next__ and returning self from __iter__ in this case, since you're not using yield the __iter__ method returns self and __next__ handles the logic of returning values:

class Item:

   def __init__(self):
      self.val = 0

   def __iter__(self):
      return self

   def __next__(self):
      if self.val > 2: raise StopIteration
      res = range(0, 30, 10)[self.val]
      self.val += 1
      return res

This also uses an auxiliary val to get the result from the range and check if we should still be iterating (if not, we raise StopIteration):

>>> for i in Item():
...    print(i)
0
10
20

The problem with this approach is that it is a one time ride, after iterating once, the self.val points to 3 and iteration can't be performed again. (using yield resolves this issue). (Yes, you could go and set val to 0 but that's just being sneaky.)

Many times looping: create custom iterator object.

The second approach is to use a custom iterator object specifically for your Item class and return it from Item.__iter__ instead of self:

class Item:
    def __iter__(self):
        return IterItem()


class IterItem:
   def __init__(self):
      self.val = 0
   def __iter__(self):
      return self
   def __next__(self):
      if self.val > 2: raise StopIteration
      res = range(0, 30, 10)[self.val]
      self.val += 1
      return res

Now every time you iterate a new custom iterator is supplied and you can support multiple iterations over Item objects.