Iteration protocol#

iterable - object that can return elements one at a time. For Python, it is any object that has __iter__ or __getitem__ method. If an object has __iter__ method, the iterable becomes an iterator by calling iter(name) where name - name of iterable. If __iter__ method is not present, Python iterates elements using __getitem__ (also by calling iter function).

class Items:
    def __init__(self, items):
        self.items = items

    def __getitem__(self, index):
        print('Вызываю __getitem__')
        return self.items[index]


In [2]: iterable_1 = Items([1, 2, 3, 4])

In [3]: iterable_1[0]
Calling __getitem__
Out[3]: 1

In [4]: for i in iterable_1:
   ...:     print('>>>>', i)
   ...:
Calling __getitem__
>>>> 1
Calling __getitem__
>>>> 2
Calling __getitem__
>>>> 3
Calling __getitem__
>>>> 4
Calling __getitem__

In [5]: list(map(str, iterable_1))
Calling __getitem__
Calling __getitem__
Calling __getitem__
Calling __getitem__
Calling __getitem__
Out[5]: ['1', '2', '3', '4']

If object has __iter__ method (which must return iterator), it is used for values iteration:

class Items:
    def __init__(self, items):
        self.items = items

    def __getitem__(self, index):
        print('Вызываю __getitem__')
        return self.items[index]

    def __iter__(self):
        print('Вызываю __iter__')
        return iter(self.items)


In [12]: iterable_1 = Items([1, 2, 3, 4])

In [13]: for i in iterable_1:
     ...:     print('>>>>', i)
     ...:
Calling __iter__
>>>> 1
>>>> 2
>>>> 3
>>>> 4

In [14]: list(map(str, iterable_1))
Calling __iter__
Out[14]: ['1', '2', '3', '4']

In Python, iter function is responsible for getting an iterator:

In [1]: lista = [1, 2, 3]

In [2]: iter(lista)
Out[2]: <list_iterator at 0xb4ede28c>

iter function will work on any object that has __iter__ or __getitem__ method. Method __iter__ returns an iterator. If this method is not available, iter function checks availability of __getitem__ method that can get elements by index. If __getitem__ method exists, elements will be iterated through index (starting with 0).

iterator - object that returns its elements one at a time. From Python point of view, it is any object that has __next__ method. This method returns the next item if any or raises Stopiteration exception when items are ended. In addition, iterator remembers which object it stopped at in the last iteration. Each iterator also has __iter__ method - that is, every iterator is an iterable object. This method returns iterator itself.

An example of creating iterator from list:

In [3]: lista = [1, 2, 3]

In [4]: i = iter(lista)

Now you can use next function that calls __next__ method to take the next element:

In [5]: next(i)
Out[5]: 1

In [6]: next(i)
Out[6]: 2

In [7]: next(i)
Out[7]: 3

In [8]: next(i)
------------------------------------------------------------
StopIteration              Traceback (most recent call last)
<ipython-input-8-bed2471d02c1> in <module>()
----> 1 next(i)

StopIteration:

After elements are ended, Stopiteration exception is raised. In order for iterator to return elements again, it has to be re-created. Similar steps are performed when for loop iterates items in the list:

In [9]: for item in lista:
   ...:     print(item)
   ...:
1
2
3

When we iterate list items, iter function is first applied to the list to create an iterator and then __next__ method is called until Stopiteration exception raised.

An example of my_for function that works with any iterable and loosely imitates built-in function for (actually gititem are iterated over by iter function):

def my_for(iterable):
    if getattr(iterable, "__iter__", None):
        print('Есть __iter__')
        iterator = iter(iterable)
        while True:
            try:
                print(next(iterator))
            except StopIteration:
                break
    elif getattr(iterable, "__getitem__", None):
        print('Нет __iter__, но есть __getitem__')
        index = 0
        while True:
            try:
                print(iterable[index])
                index += 1
            except IndexError:
                break

Check function on object that has __iter__:

In [18]: my_for([1, 2, 3, 4])
Есть __iter__
1
2
3
4

Check function on object that does not have __iter__ but has __getitem__:

class Items:
    def __init__(self, items):
        self.items = items

    def __getitem__(self, index):
        print('Вызываю __getitem__')
        return self.items[index]


In [20]: iterable_1 = Items([1, 2, 3, 4, 5])

In [21]: my_for(iterable_1)
Нет __iter__, но есть __getitem__
Calling __getitem__
1
Calling __getitem__
2
Calling __getitem__
3
Calling __getitem__
4
Calling __getitem__
5
Calling __getitem__

Iterator creation#

Example of Network class:

In [10]: import ipaddress
    ...:
    ...: class Network:
    ...:     def __init__(self, network):
    ...:         self.network = network
    ...:         subnet = ipaddress.ip_network(self.network)
    ...:         self.addresses = [str(ip) for ip in subnet.hosts()]

Example of Network class instance creation:

In [14]: net1 = Network('10.1.1.192/30')

In [15]: net1
Out[15]: <__main__.Network at 0xb3124a6c>

In [16]: net1.addresses
Out[16]: ['10.1.1.193', '10.1.1.194']

In [17]: net1.network
Out[17]: '10.1.1.192/30'

Create an iterator from Network class:

class Network:
    def __init__(self, network):
        self.network = network
        subnet = ipaddress.ip_network(self.network)
        self.addresses = [str(ip) for ip in subnet.hosts()]
        self._index = 0

    def __iter__(self):
        print('Вызываю __iter__')
        return self

    def __next__(self):
        print('Вызываю __next__')
        if self._index < len(self.addresses):
            current_address = self.addresses[self._index]
            self._index += 1
            return current_address
        else:
            raise StopIteration

Method __iter__ in iterator must return object itself, therefore return self is specified in method and __next__ method returns elements one at a time and raises StopIteration exception when elements have run out.

In [14]: net1 = Network('10.1.1.192/30')

In [15]: for ip in net1:
    ...:     print(ip)
    ...:
Calling __iter__
Calling __next__
10.1.1.193
Calling __next__
10.1.1.194
Calling __next__

Most of the time, iterator is a disposable object and once we’ve iterated elements, we can’t do it again:

In [16]: for ip in net1:
    ...:     print(ip)
    ...:
Calling __iter__
Calling __next__

Creation of iterable#

Very often it is sufficient for class to be an iterable and not necessarily an iterator. If an object is iterable, it can be used in for loop, map functions, filter, sorted, enumerate and others. It is also generally easier to make an iterable than an iterator.

In order for Network class to be iterable, class must have __iter__ (__next__ is not needed) and method must return iterator. Since in this case, Network iterates addresses that are in self.addresses list, the easiest option to return iterator is to return iter(self.addresses):

class Network:
    def __init__(self, network):
        self.network = network
        subnet = ipaddress.ip_network(self.network)
        self.addresses = [str(ip) for ip in subnet.hosts()]

    def __iter__(self):
        return iter(self.addresses)

Now all Network class instances will be iterable objects:

In [18]: net1 = Network('10.1.1.192/30')

In [19]: for ip in net1:
    ...:     print(ip)
    ...:
10.1.1.193
10.1.1.194