web-dev-qa-db-fra.com

Comment écrire une classe de générateur?

Je vois beaucoup d'exemples de fonctions de générateur, mais je veux savoir comment écrire des générateurs pour les classes. Disons que je voulais écrire la série de Fibonacci en classe.

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))

Sortie:

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

Pourquoi la valeur self.a ne pas être imprimé? Aussi, comment puis-je écrire unittest pour les générateurs?

27
Pritam

Comment écrire une classe de générateur?

Vous y êtes presque, écrivez une classe Iterator (je montre un générateur à la fin de la réponse), mais __next__ Est appelée chaque fois que vous appelez l'objet avec next, vous retournez un objet générateur. Au lieu de cela, utilisez __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

Pour faire de la classe elle-même un itérateur:

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

Et maintenant:

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

Pourquoi la valeur self.a n'est-elle pas imprimée?

Voici votre code original avec mes commentaires:

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))

Ainsi, chaque fois que vous avez appelé next(f), vous avez obtenu l'objet générateur que __next__ Renvoie:

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

Aussi, comment puis-je écrire unittest pour les générateurs?

Vous devez toujours implémenter une méthode d'envoi et de projection pour un 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)

Et maintenant:

>>> 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>

Implémentons donc un objet générateur et exploitons la classe de base abstraite Generator du module collections (voir la source de son implementation ), ce qui signifie que nous devons seulement implémenter send et throw - nous donnant close, __iter__ (se retourne soi-même) et __next__ (comme .send(None)) gratuitement (voir le modèle de données Python sur les coroutines ):

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

et en utilisant les mêmes tests ci-dessus:

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

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

Python 2

L'ABC Generator n'est que dans Python 3. Pour ce faire sans Generator, nous devons écrire au moins close, __iter__ Et __next__ En plus des méthodes que nous avons définies ci-dessus.

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")

Notez que j'ai copié close directement à partir de Python 3 bibliothèque standard) , sans modification.

40
Aaron Hall

__next__ devrait retourner un élément, pas le céder.

Vous pouvez soit écrire ce qui suit, dans lequel Fib.__iter__ retourne un itérateur approprié:

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 faire de chaque instance elle-même un itérateur en définissant __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
chepner

Ne pas utiliser yield dans __next__ fonction et implémentation next également pour compatibilité avec python2.7 +

Code

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__()

Si vous donnez à la classe une méthode __iter__()implémentée en tant que générateur , elle retournera automatiquement un objet générateur à l'appel, donc que l'objet __iter__ et __next__ méthodes seront celles utilisées.

Voici ce que je veux dire:

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

Sortie:

0
1
1
2
3
5
1
martineau