Como escrever uma aula de gerador?

Vejo muitos exemplos de funções geradoras, mas quero saber como escrever geradores para as aulas. Digamos que queria escrever a série Fibonacci como turma.

class Fib:
    def __init__(self):
        self.a, self.b = 0, 1

    def __next__(self):
        yield self.a
        self.a, self.b = self.b, self.a+self.b

f = Fib()

for i in range(3):
    print(next(f))

resultado:

<generator object __next__ at 0x000000000A3E4F68>
<generator object __next__ at 0x000000000A3E4F68>
<generator object __next__ at 0x000000000A3E4F68>

porque é que o valor {[2] } não está a ser impresso? Além disso, como escrevo para geradores?

Author: martineau, 2017-03-23

4 answers

Como escrever uma aula de gerador?

Você está quase lá, escrevendo uma classe Iterator (eu mostro um gerador no final da resposta), mas __next__ é chamado cada vez que você chama o objeto com next, retornando um objeto gerador. Em vez disso, use __iter__:

>>> class Fib:
...     def __init__(self):
...         self.a, self.b = 0, 1
...     def __iter__(self):
...         while True:
...             yield self.a
...             self.a, self.b = self.b, self.a+self.b
...
>>> f = iter(Fib())
>>> for i in range(3):
...     print(next(f))
...
0
1
1

Para fazer da própria classe Um iterador:

class Fib:
    def __init__(self):
        self.a, self.b = 0, 1        
    def __next__(self):
        return_value = self.a
        self.a, self.b = self.b, self.a+self.b
        return return_value
    def __iter__(self):
        return self

E agora:

>>> f = iter(Fib())
>>> for i in range(3):
...     print(next(f))
...
0
1
1

Porque é o valor de si mesmo.a não ser impresso?

Aqui está o teu código original com os meus comentários:
class Fib:
    def __init__(self):
        self.a, self.b = 0, 1

    def __next__(self):
        yield self.a          # yield makes .__next__() return a generator!
        self.a, self.b = self.b, self.a+self.b

f = Fib()

for i in range(3):
    print(next(f))

Sempre que ligaste para {[13] } tens o objecto gerador que __next__ devolve:

<generator object __next__ at 0x000000000A3E4F68>
<generator object __next__ at 0x000000000A3E4F68>
<generator object __next__ at 0x000000000A3E4F68>
Além disso, como escrevo o mais apto para geradores?
Ainda precisa de implementar um método de enviar e lançar para um Generator
from collections import Iterator, Generator
import unittest

class Test(unittest.TestCase):
    def test_Fib(self):
        f = Fib()
        self.assertEqual(next(f), 0)
        self.assertEqual(next(f), 1)
        self.assertEqual(next(f), 1)
        self.assertEqual(next(f), 2) #etc...
    def test_Fib_is_iterator(self):
        f = Fib()
        self.assertIsInstance(f, Iterator)
    def test_Fib_is_generator(self):
        f = Fib()
        self.assertIsInstance(f, Generator)

E agora:

>>> unittest.main(exit=False)
..F
======================================================================
FAIL: test_Fib_is_generator (__main__.Test)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<stdin>", line 7, in test_Fib_is_generator
AssertionError: <__main__.Fib object at 0x00000000031A6320> is not an instance of <class 'collections.abc.Generator'>

----------------------------------------------------------------------
Ran 3 tests in 0.001s

FAILED (failures=1)
<unittest.main.TestProgram object at 0x0000000002CAC780>
Então vamos implementar um objeto gerador, e alavancar a classe base abstrata do módulo de coleções (veja a fonte para o seu O que significa que só precisamos de implementar {[[17]} e throw - dando-nos close, __iter__ (devolve o self), e __next__ (o mesmo que .send(None)) GRATUITAMENTE (veja o modelo de dados em Python nas coroutinas):
class Fib(Generator):
    def __init__(self):
        self.a, self.b = 0, 1        
    def send(self, ignored_arg):
        return_value = self.a
        self.a, self.b = self.b, self.a+self.b
        return return_value
    def throw(self, type=None, value=None, traceback=None):
        raise StopIteration

E utilizando os mesmos testes acima:

>>> unittest.main(exit=False)
...
----------------------------------------------------------------------
Ran 3 tests in 0.002s

OK
<unittest.main.TestProgram object at 0x00000000031F7CC0>

Python 2

O ABC Generator está apenas no Python 3. Para fazer isso sem Generator, precisamos escrever pelo menos close, __iter__, e __next__ além dos métodos definidos acima.

class Fib(object):
    def __init__(self):
        self.a, self.b = 0, 1        
    def send(self, ignored_arg):
        return_value = self.a
        self.a, self.b = self.b, self.a+self.b
        return return_value
    def throw(self, type=None, value=None, traceback=None):
        raise StopIteration
    def __iter__(self):
        return self
    def next(self):
        return self.send(None)
    def close(self):
        """Raise GeneratorExit inside generator.
        """
        try:
            self.throw(GeneratorExit)
        except (GeneratorExit, StopIteration):
            pass
        else:
            raise RuntimeError("generator ignored GeneratorExit")

Note que copiei close directamente da Biblioteca Padrão Python 3 , sem modificações.

 26
Author: Aaron Hall, 2017-08-14 20:21:35

__next__ SE devolver um item, não o entregar.

Pode escrever o seguinte, no qual Fib.__iter__ devolve um iterador adequado:

class Fib:
    def __init__(self, n):
        self.n = n
        self.a, self.b = 0, 1

    def __iter__(self):
        for i in range(self.n):
            yield self.a
            self.a, self.b = self.b, self.a+self.b

f = Fib(10)

for i in f:
    print i

Ou fazer de cada instância um iterador definindo __next__.

class Fib:
    def __init__(self):
        self.a, self.b = 0, 1

    def __iter__(self):
        return self

    def __next__(self):
        x = self.a
        self.a, self.b = self.b, self.a + self.b
        return x

f = Fib()

for i in range(10):
    print next(f)
 3
Author: chepner, 2017-03-23 18:06:55

Não utilize yield na função __next__ e implemente next também para compatibilidade com python2. 7+

Código

class Fib:
    def __init__(self):
        self.a, self.b = 0, 1
    def __next__(self):
        a = self.a
        self.a, self.b = self.b, self.a+self.b
        return a
    def next(self):
        return self.__next__()
 3
Author: Sarath Sadasivan Pillai, 2017-03-23 18:09:44

Se você der à classe um método __iter__() implementado como um gerador , ele irá devolver automaticamente um objecto gerador quando chamado, então que os métodos do objecto __iter__ e __next__ serão os utilizados.

Eis o que quero dizer:
class Fib:
    def __init__(self):
        self.a, self.b = 0, 1

    def __iter__(self):
        while True:
            value, self.a, self.b = self.a, self.b, self.a+self.b
            yield value

f = Fib()

for i, value in enumerate(f, 1):
    print(value)
    if i > 5:
        break

Resultado:

0
1
1
2
3
5
 1
Author: martineau, 2017-07-28 13:26:02