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