Python Iterators e Generators #dicaDeSexta

Vou fazer uma breve abordagem sobre o uso de Iteratos e Generators

Para iniciar, vamos fazer uma implementação simples do que seria o range do python.

 
 1 def my_range(start, stop=None, step=1):
 2     if stop is None:
 3         stop = start
 4         start = 0
 5 
 6     ret = []
 7 
 8     while start < stop:
 9         ret.append(start)
10         start += step
11 
12     return ret
13 
14 
15 for i in my_range(100):
16     print(i)

O problema dessa implementação é que ele gera um list desnecessário, vamos supor que queremos gerar um my_range(10000000), ele irá retornar um list com 10000000 de células. É um desperdício de memória.

Iterator

Um iterator, segundo a documentação do Python é "um objeto que representa um fluxo de dados. Repetidas chamadas ao método next do objeto iterator retorna o item seguinte. Quando não há mais dados é lançado a exception StopIteration.". Para um objeto ser iterator é necessário que ele contenha o método __iter__, grande parte das implementações desse método retorna self. Vejamos a implementação do my_range com iterator.

 1 class my_xrange(object):
 2     def __init__(self, start, stop=None, step=1):
 3         if stop is None:
 4             stop = start
 5             start = 0
 6 
 7         self.start = start
 8         self.stop = stop
 9         self.step = step
10         self.current = start
11 
12     def __iter__(self):
13         return self
14 
15     # Python 3
16     def __next__(self):
17         if self.current < self.stop:
18             ret = self.current
19             self.current += 1
20             return ret
21         raise StopIteration()
22 
23     # Python 2 / Compatibilizando
24     def next(self):
25         return self.__next__()
26 
27 
28 for i in my_xrange(100):
29     print(i)

Beleza! Resolvemos o problema de memória, mas... É muito código pra pouca coisa! A solução está nos generators!

Generators

Segundo a documentação do Python generators "permitem que você declare uma função que se comporta como um iterador, ou seja, ele pode ser usado em um loop". No lugar de return, usamos yield.


 1 def my_xrange(start, stop=None, step=1):
 2     if stop is None:
 3         stop = start
 4         start = 0
 5 
 6     while start < stop:
 7         yield start
 8         start += step
 9 
10 
11 for i in my_xrange(100):
12     print(i)

O código ficou muito mais limpo. Quando a palavra chave yield está dentro de uma função, esta função se torna um iterator. Para demonstrar o funcionamento do iterator coloquei uns prints no código acima, para entendermos o seu funcionamento.

 1 def my_xrange(start, stop=None, step=1):
 2     if stop is None:
 3         stop = start
 4         start = 0
 5 
 6     print('my_xrange iniciado.')
 7     while start < stop:
 8         print('código bloqueado no yield')
 9         yield start
10         print('código liberado após iteração')
11         start += step
12     print('acabei')
13 
14 
15 the_range = my_xrange(2)
16 print('Vou mostrar o primeiro item do the_range')
17 print(the_range.__next__())
18 print('----------------------------------------')
19 
20 # para o python2 use
21 # print(the_range.next())
22 
23 print('Vou iterar o the_range')
24 print('----------------------------------------')
25 print(the_range.__next__())
26 print('----------------------------------------')
27 print(the_range.__next__())

O output será o seguinte:

Vou mostrar o primeiro item do the_range
my_xrange iniciado.
código bloqueado no yield
0
----------------------------------------
Vou iterar o the_range
----------------------------------------
código liberado após iteração
código bloqueado no yield
1
----------------------------------------
código liberado após iteração
acabei
Traceback (most recent call last):
  File "my_xrange_stoped.py", line 27, in 
    print(the_range.__next__())
StopIteration

Repare que quando chamamos o método my_xrange(2), o print que é exibido no começo do corpo da função 'my_xrange iniciado.' (linha 6) não foi exibido no output.
Isso porque quando chamamos o método, o Python não chama a função, ele gera um iterator. A função só é executada quando o método __next__ (Python3) ou next (Python2) é chamado. Quando o __next__ é chamado ele libera a execução da função, se a execução chegar a algum yield o valor do yield é retornado, senão, é lançado o StopIteration, como no nosso exemplo nosso loop ia até dois, na terceira chamada ao método __next__ foi lançado o exception StopIterator.

Nenhum comentário: