Différence entre les générateurs et les itérateurs de Python

538

Quelle est la différence entre les itérateurs et les générateurs? Quelques exemples d'utilisation de chaque cas seraient utiles.

newToProgramming
la source

Réponses:

544

iteratorest un concept plus général: tout objet dont la classe a une nextméthode ( __next__en Python 3) et une __iter__méthode qui en a return self.

Chaque générateur est un itérateur, mais pas l'inverse. Un générateur est construit en appelant une fonction qui a une ou plusieurs yieldexpressions ( yieldinstructions, en Python 2.5 et versions antérieures), et est un objet qui répond à la définition d'un iterator.

Vous voudrez peut-être utiliser un itérateur personnalisé, plutôt qu'un générateur, lorsque vous avez besoin d'une classe avec un comportement de maintien d'état quelque peu complexe, ou souhaitez exposer d'autres méthodes en plus next(et __iter__et __init__). Le plus souvent, un générateur (parfois, pour des besoins suffisamment simples, une expression de générateur ) est suffisant, et il est plus simple de coder car la maintenance de l'état (dans des limites raisonnables) est fondamentalement "faite pour vous" par la suspension et la reprise du cadre.

Par exemple, un générateur tel que:

def squares(start, stop):
    for i in range(start, stop):
        yield i * i

generator = squares(a, b)

ou l'expression de générateur équivalent (genexp)

generator = (i*i for i in range(a, b))

prendrait plus de code à construire comme un itérateur personnalisé:

class Squares(object):
    def __init__(self, start, stop):
       self.start = start
       self.stop = stop
    def __iter__(self): return self
    def next(self): # __next__ in Python 3
       if self.start >= self.stop:
           raise StopIteration
       current = self.start * self.start
       self.start += 1
       return current

iterator = Squares(a, b)

Mais, bien sûr, avec la classe, Squaresvous pourriez facilement proposer des méthodes supplémentaires, à savoir

    def current(self):
       return self.start

si vous avez réellement besoin de telles fonctionnalités supplémentaires dans votre application.

Alex Martelli
la source
comment utiliser l'itérateur une fois que je l'ai créé?
Vincenzooo
@Vincenzooo qui dépend de ce que vous voulez en faire. Cela fera partie d'un for ... in ...:, passé à une fonction, ou vous appellereziter.next()
Caleth
@Caleth, je posais des questions sur la syntaxe exacte, car je recevais une erreur en essayant d'utiliser la for..insyntaxe. Peut-être que je manquais quelque chose, mais c'était il y a quelque temps, je ne me souviens pas si j'ai résolu. Je vous remercie!
Vincenzooo
137

Quelle est la différence entre les itérateurs et les générateurs? Quelques exemples d'utilisation de chaque cas seraient utiles.

En résumé: les itérateurs sont des objets qui ont une __iter__et une méthode __next__( nexten Python 2). Les générateurs offrent un moyen simple et intégré de créer des instances d'itérateurs.

Une fonction avec un rendement est toujours une fonction qui, lorsqu'elle est appelée, renvoie une instance d'un objet générateur:

def a_function():
    "when called, returns generator object"
    yield

Une expression de générateur renvoie également un générateur:

a_generator = (i for i in range(0))

Pour une exposition et des exemples plus approfondis, continuez à lire.

Un générateur est un itérateur

Plus précisément, le générateur est un sous-type d'itérateur.

>>> import collections, types
>>> issubclass(types.GeneratorType, collections.Iterator)
True

Nous pouvons créer un générateur de plusieurs façons. Une façon très courante et simple de le faire est d'utiliser une fonction.

Plus précisément, une fonction avec un rendement est une fonction qui, lorsqu'elle est appelée, renvoie un générateur:

>>> def a_function():
        "just a function definition with yield in it"
        yield
>>> type(a_function)
<class 'function'>
>>> a_generator = a_function()  # when called
>>> type(a_generator)           # returns a generator
<class 'generator'>

Et un générateur, encore une fois, est un itérateur:

>>> isinstance(a_generator, collections.Iterator)
True

Un itérateur est un itérable

Un itérateur est un itérable,

>>> issubclass(collections.Iterator, collections.Iterable)
True

qui nécessite une __iter__méthode qui retourne un Iterator:

>>> collections.Iterable()
Traceback (most recent call last):
  File "<pyshell#79>", line 1, in <module>
    collections.Iterable()
TypeError: Can't instantiate abstract class Iterable with abstract methods __iter__

Quelques exemples d'itérables sont les tuples intégrés, les listes, les dictionnaires, les ensembles, les ensembles figés, les chaînes, les chaînes d'octets, les tableaux d'octets, les plages et les vues de mémoire:

>>> all(isinstance(element, collections.Iterable) for element in (
        (), [], {}, set(), frozenset(), '', b'', bytearray(), range(0), memoryview(b'')))
True

Les itérateurs nécessitent une méthode nextou__next__

En Python 2:

>>> collections.Iterator()
Traceback (most recent call last):
  File "<pyshell#80>", line 1, in <module>
    collections.Iterator()
TypeError: Can't instantiate abstract class Iterator with abstract methods next

Et en Python 3:

>>> collections.Iterator()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class Iterator with abstract methods __next__

Nous pouvons obtenir les itérateurs des objets intégrés (ou objets personnalisés) avec la iterfonction:

>>> all(isinstance(iter(element), collections.Iterator) for element in (
        (), [], {}, set(), frozenset(), '', b'', bytearray(), range(0), memoryview(b'')))
True

La __iter__méthode est appelée lorsque vous essayez d'utiliser un objet avec une boucle for. Ensuite, la __next__méthode est appelée sur l'objet itérateur pour extraire chaque élément de la boucle. L'itérateur se lève StopIterationlorsque vous l'avez épuisé et il ne peut pas être réutilisé à ce stade.

De la documentation

Dans la section Types génératrice de la section Types Itérateur des types intégrés documents :

Les générateurs de Python fournissent un moyen pratique d'implémenter le protocole itérateur. Si la __iter__()méthode d' un objet conteneur est implémentée en tant que générateur, il retournera automatiquement un objet itérateur (techniquement, un objet générateur) fournissant les méthodes __iter__()et next()[ __next__()en Python 3]. Plus d'informations sur les générateurs peuvent être trouvées dans la documentation de l'expression de rendement.

(Je souligne.)

Nous apprenons donc que les générateurs sont un type (pratique) d'itérateur.

Exemples d'objets d'itérateur

Vous pouvez créer un objet qui implémente le protocole Iterator en créant ou en étendant votre propre objet.

class Yes(collections.Iterator):

    def __init__(self, stop):
        self.x = 0
        self.stop = stop

    def __iter__(self):
        return self

    def next(self):
        if self.x < self.stop:
            self.x += 1
            return 'yes'
        else:
            # Iterators must raise when done, else considered broken
            raise StopIteration

    __next__ = next # Python 3 compatibility

Mais il est plus facile d'utiliser simplement un générateur pour ce faire:

def yes(stop):
    for _ in range(stop):
        yield 'yes'

Ou peut-être plus simple, une expression de générateur (fonctionne de manière similaire aux listes de compréhension):

yes_expr = ('yes' for _ in range(stop))

Ils peuvent tous être utilisés de la même manière:

>>> stop = 4             
>>> for i, y1, y2, y3 in zip(range(stop), Yes(stop), yes(stop), 
                             ('yes' for _ in range(stop))):
...     print('{0}: {1} == {2} == {3}'.format(i, y1, y2, y3))
...     
0: yes == yes == yes
1: yes == yes == yes
2: yes == yes == yes
3: yes == yes == yes

Conclusion

Vous pouvez utiliser le protocole Iterator directement lorsque vous devez étendre un objet Python en tant qu'objet pouvant être itéré.

Cependant, dans la grande majorité des cas, vous êtes le mieux placé yieldpour définir une fonction qui renvoie un itérateur de générateur ou considérer les expressions de générateur.

Enfin, notez que les générateurs offrent encore plus de fonctionnalités que les coroutines. J'explique Generators, ainsi que la yielddéclaration, en détail sur ma réponse à "Que fait le mot-clé" yield "?".

Aaron Hall
la source
41

Itérateurs:

Les itérateurs sont des objets qui utilisent la next()méthode pour obtenir la valeur suivante de la séquence.

Générateurs:

Un générateur est une fonction qui produit ou produit une séquence de valeurs en utilisant yield méthode.

Chaque next()méthode faisant appel à un objet générateur (par exemple: fcomme dans l'exemple ci-dessous) renvoyé par la fonction générateur (par exemple: foo()fonction dans l'exemple ci-dessous), génère la valeur suivante en séquence.

Lorsqu'une fonction de générateur est appelée, elle retourne un objet générateur sans même commencer l'exécution de la fonction. Lorsque la next()méthode est appelée pour la première fois, la fonction commence à s'exécuter jusqu'à ce qu'elle atteigne l'instruction yield qui renvoie la valeur produite. Le rendement garde une trace de ie se souvient de la dernière exécution. Et le deuxième next()appel continue à partir de la valeur précédente.

L'exemple suivant illustre l'interaction entre le rendement et l'appel à la méthode suivante sur l'objet générateur.

>>> def foo():
...     print "begin"
...     for i in range(3):
...         print "before yield", i
...         yield i
...         print "after yield", i
...     print "end"
...
>>> f = foo()
>>> f.next()
begin
before yield 0            # Control is in for loop
0
>>> f.next()
after yield 0             
before yield 1            # Continue for loop
1
>>> f.next()
after yield 1
before yield 2
2
>>> f.next()
after yield 2
end
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>>

la source
3
Le rendement juste pour info n'est pas une méthode, c'est un mot
Jay Parikh
25

Ajouter une réponse car aucune des réponses existantes ne traite spécifiquement de la confusion dans la littérature officielle.

Les fonctions de générateur sont des fonctions ordinaires définies à l'aide deyieldau lieu dereturn. Lorsqu'elle est appelée, une fonction de générateur renvoie un objet générateur , qui est une sorte d'itérateur - il a unenext()méthode. Quand vous appeleznext() , la valeur suivante fournie par la fonction générateur est renvoyée.

La fonction ou l'objet peut être appelé le "générateur" selon le document source Python que vous lisez. Le glossaire Python dit les fonctions du générateur, tandis que le wiki Python implique les objets du générateur. Le tutoriel Python parvient remarquablement à impliquer les deux usages en l'espace de trois phrases:

Les générateurs sont un outil simple et puissant pour créer des itérateurs. Ils sont écrits comme des fonctions normales, mais utilisent l'instruction yield chaque fois qu'ils souhaitent renvoyer des données. Chaque fois que next () est appelé, le générateur reprend là où il s'était arrêté (il se souvient de toutes les valeurs de données et de la dernière instruction exécutée).

Les deux premières phrases identifient les générateurs avec des fonctions génératrices, tandis que la troisième phrase les identifie avec des objets générateurs.

Malgré toute cette confusion, on peut rechercher la référence du langage Python pour le mot clair et final:

L'expression yield n'est utilisée que lors de la définition d'une fonction de générateur et ne peut être utilisée que dans le corps d'une définition de fonction. L'utilisation d'une expression de rendement dans une définition de fonction est suffisante pour que cette définition crée une fonction de générateur au lieu d'une fonction normale.

Lorsqu'une fonction de générateur est appelée, elle renvoie un itérateur appelé générateur. Ce générateur contrôle ensuite l'exécution d'une fonction de générateur.

Ainsi, dans un usage formel et précis, "générateur" non qualifié signifie objet générateur, pas fonction générateur.

Les références ci-dessus sont pour Python 2 mais la référence du langage Python 3 dit la même chose. Cependant, le glossaire Python 3 indique que

générateur ... Fait généralement référence à une fonction de générateur, mais peut faire référence à un itérateur de générateur dans certains contextes. Dans les cas où la signification souhaitée n'est pas claire, l'utilisation des termes complets évite toute ambiguïté.

Paul
la source
Je ne pense pas qu'il y ait beaucoup de confusion entre les fonctions de générateur et les objets de générateur, pour la même raison, il n'y a généralement pas de confusion entre les classes et leurs instances. Dans les deux cas, vous appelez l'un pour obtenir l'autre, et dans une conversation informelle (ou une documentation écrite rapidement), vous pouvez utiliser le nom de la classe ou le mot "générateur" pour l'un ou l'autre. Vous n'avez besoin d'être explicite sur la "fonction générateur" que sur "l'objet générateur" dans de rares situations où celle dont vous parlez est importante.
Blckknght
6
1. Indépendamment des raisons théoriques pour lesquelles il ne devrait pas y avoir de confusion, les commentaires sur d'autres réponses à cette question se nient et se contredisent sans résolution, indiquant une confusion réelle. 2. L'imprécision occasionnelle est une bonne chose, mais une source précise et faisant autorité devrait au moins être l'une des options de l'OS. J'utilise à la fois des fonctions de générateur et des objets dans mon projet actuel, et la distinction est très importante lors de la conception et du codage. Il est bon de savoir quelle terminologie utiliser maintenant, donc je n'ai pas besoin de changer des dizaines de noms de variables et de commentaires plus tard.
Paul
2
Imaginez une littérature mathématique où aucune distinction n'est faite entre une fonction et sa valeur de retour. Il est parfois pratique de les confondre de manière informelle, mais cela augmente le risque de diverses erreurs. Les mathématiques modernes avancées seraient considérablement et inutilement entravées si la distinction n'était pas formalisée dans les conventions, le langage et la notation.
Paul
2
Les fonctions d'ordre supérieur passant autour des générateurs ou des fonctions de générateur peuvent sembler bizarres, mais pour moi, elles sont apparues. Je travaille dans Apache Spark et il applique un style de programmation très fonctionnel. Les fonctions doivent créer, transmettre et distribuer toutes sortes d'objets pour faire avancer les choses. J'ai eu un certain nombre de situations où j'ai perdu de vue le type de "générateur" avec lequel je travaillais. Des indices dans les noms de variables et les commentaires, utilisant la terminologie cohérente et correcte, ont aidé à dissiper la confusion. L'obscurité d'un pythoniste peut être au centre de la conception d'un autre projet!
Paul
1
@Paul, merci d'avoir écrit cette réponse. Cette confusion est importante car la différence entre un objet générateur et une fonction de générateur est la différence entre obtenir le comportement souhaité et devoir rechercher des générateurs.
blujay
15

Tout le monde a une réponse très agréable et verbeuse avec des exemples et je l'apprécie vraiment. Je voulais juste donner une réponse en quelques lignes pour les personnes qui ne sont pas encore assez claires conceptuellement:

Si vous créez votre propre itérateur, il est un peu impliqué - vous devez créer une classe et au moins implémenter l'itérateur et les méthodes suivantes. Mais que se passe-t-il si vous ne voulez pas passer par ces tracas et que vous souhaitez créer rapidement un itérateur. Heureusement, Python fournit un moyen raccourci de définir un itérateur. Tout ce que vous avez à faire est de définir une fonction avec au moins 1 appel à céder et maintenant lorsque vous appelez cette fonction, elle renverra " quelque chose " qui agira comme un itérateur (vous pouvez appeler la méthode suivante et l'utiliser dans une boucle for). Ce quelque chose a un nom en Python appelé Generator

J'espère que cela clarifie un peu.

Heapify
la source
10

Les réponses précédentes ont manqué cet ajout: un générateur a une closeméthode, contrairement aux itérateurs typiques. La closeméthode déclenche une StopIterationexception dans le générateur, qui peut être interceptée dans unfinally clause de cet itérateur, pour avoir la possibilité d'exécuter un nettoyage. Cette abstraction la rend plus utilisable dans les grands itérateurs que les simples. On peut fermer un générateur comme on pourrait fermer un fichier, sans avoir à se soucier de ce qui se trouve en dessous.

Cela dit, ma réponse personnelle à la première question serait: itérable n'a qu'une __iter__méthode, les itérateurs typiques ont une __next__méthode uniquement, les générateurs ont à la fois un __iter__et un __next__et un supplémentaireclose .

Pour la deuxième question, ma réponse personnelle serait: dans une interface publique, j'ai tendance à privilégier beaucoup les générateurs, car c'est plus résilient: la closeméthode une plus grande composabilité avecyield from . Localement, je peux utiliser des itérateurs, mais seulement si c'est une structure plate et simple (les itérateurs ne se composent pas facilement) et s'il y a des raisons de croire que la séquence est plutôt courte surtout si elle peut être arrêtée avant d'atteindre la fin. J'ai tendance à considérer les itérateurs comme une primitive de bas niveau, sauf comme des littéraux.

Pour les questions de flux de contrôle, les générateurs sont un concept aussi important que promis: les deux sont abstraits et composables.

Hibou57
la source
Pourriez-vous donner un exemple pour illustrer ce que vous voulez dire lorsque vous parlez de composition? Pouvez-vous également expliquer ce que vous avez en tête lorsque vous parlez d '" itérateurs typiques "?
bli
1
Une autre réponse ( stackoverflow.com/a/28353158/1878788 ) indique qu '"un itérateur est un itérable". Puisqu'un itérable a une __iter__méthode, comment se fait-il qu'un itérateur puisse avoir __next__seulement? S'ils sont censés être des itérables, je m'attendrais à ce qu'ils en aient nécessairement __iter__aussi.
bli
1
@bli: AFAICS cette réponse se réfère ici à la norme PEP234 , donc elle est correcte, tandis que l'autre réponse fait référence à une certaine implémentation, donc elle est discutable. La norme ne nécessite qu'un __iter__on iterables pour retourner un itérateur, qui ne nécessite qu'une nextméthode ( __next__en Python3). Veuillez ne pas confondre les normes (pour le typage canard) avec leur implémentation (comment un interprète Python particulier l'a implémenté). C'est un peu comme la confusion entre les fonctions de générateur (définition) et les objets de générateur (implémentation). ;)
Tino
7

Fonction Générateur, Objet Générateur, Générateur:

Une fonction Generator est exactement comme une fonction régulière en Python mais elle contient une ou plusieurs yieldinstructions. Les fonctions de générateur sont un excellent outil pour créer des objets Iterator aussi facilement que possible. L' objet Iterator returend par générateur est également appelé objet Generator ou Generator .

Dans cet exemple, j'ai créé une fonction Generator qui renvoie un objet Generator <generator object fib at 0x01342480>. Tout comme les autres itérateurs, les objets Generator peuvent être utilisés en forboucle ou avec la fonction intégrée next()qui renvoie la valeur suivante du générateur.

def fib(max):
    a, b = 0, 1
    for i in range(max):
        yield a
        a, b = b, a + b
print(fib(10))             #<generator object fib at 0x01342480>

for i in fib(10):
    print(i)               # 0 1 1 2 3 5 8 13 21 34


print(next(myfib))         #0
print(next(myfib))         #1
print(next(myfib))         #1
print(next(myfib))         #2

Une fonction de générateur est donc le moyen le plus simple de créer un objet Iterator.

Itérateur :

Chaque objet générateur est un itérateur mais pas l'inverse. Un objet itérateur personnalisé peut être créé si sa classe implémente __iter__et __next__méthode (également appelée protocole itérateur).

Cependant, il est beaucoup plus facile d'utiliser la fonction des générateurs pour créer des itérateurs car ils simplifient leur création, mais un itérateur personnalisé vous donne plus de liberté et vous pouvez également implémenter d'autres méthodes en fonction de vos besoins, comme indiqué dans l'exemple ci-dessous.

class Fib:
    def __init__(self,max):
        self.current=0
        self.next=1
        self.max=max
        self.count=0

    def __iter__(self):
        return self

    def __next__(self):
        if self.count>self.max:
            raise StopIteration
        else:
            self.current,self.next=self.next,(self.current+self.next)
            self.count+=1
            return self.next-self.current

    def __str__(self):
        return "Generator object"

itobj=Fib(4)
print(itobj)               #Generator object

for i in Fib(4):  
    print(i)               #0 1 1 2

print(next(itobj))         #0
print(next(itobj))         #1
print(next(itobj))         #1
N Randhawa
la source
6

Exemples de Ned Batchelder fortement recommandés pour les itérateurs et les générateurs

Une méthode sans générateurs qui fait quelque chose pour les nombres pairs

def evens(stream):
   them = []
   for n in stream:
      if n % 2 == 0:
         them.append(n)
   return them

tout en utilisant un générateur

def evens(stream):
    for n in stream:
        if n % 2 == 0:
            yield n
  • Nous n'avons besoin d'aucune liste ni returndéclaration
  • Efficace pour un flux de grande longueur / infini ... il marche juste et donne la valeur

L'appel de la evensméthode (générateur) est comme d'habitude

num = [...]
for n in evens(num):
   do_smth(n)
  • Générateur également utilisé pour rompre la double boucle

Itérateur

Un livre plein de pages est un itérable , un signet est un itérateur

et ce signet n'a rien d'autre à faire que de bouger next

litr = iter([1,2,3])
next(litr) ## 1
next(litr) ## 2
next(litr) ## 3
next(litr) ## StopIteration  (Exception) as we got end of the iterator

Pour utiliser Generator ... nous avons besoin d'une fonction

Pour utiliser Iterator ... nous avons besoin nextetiter

Comme cela a été dit:

Une fonction Generator renvoie un objet itérateur

Tout l'avantage d'Iterator:

Stockez un élément à la fois dans la mémoire

Marwan Mostafa
la source
À propos de votre premier extrait de code, j'aimerais savoir quoi d'autre arg 'stream' pourrait être que la liste []?
Iqra.
5

Vous pouvez comparer les deux approches pour les mêmes données:

def myGeneratorList(n):
    for i in range(n):
        yield i

def myIterableList(n):
    ll = n*[None]
    for i in range(n):
        ll[i] = i
    return ll

# Same values
ll1 = myGeneratorList(10)
ll2 = myIterableList(10)
for i1, i2 in zip(ll1, ll2):
    print("{} {}".format(i1, i2))

# Generator can only be read once
ll1 = myGeneratorList(10)
ll2 = myIterableList(10)

print("{} {}".format(len(list(ll1)), len(ll2)))
print("{} {}".format(len(list(ll1)), len(ll2)))

# Generator can be read several times if converted into iterable
ll1 = list(myGeneratorList(10))
ll2 = myIterableList(10)

print("{} {}".format(len(list(ll1)), len(ll2)))
print("{} {}".format(len(list(ll1)), len(ll2)))

De plus, si vous vérifiez l'empreinte mémoire, le générateur prend beaucoup moins de mémoire car il n'a pas besoin de stocker toutes les valeurs en mémoire en même temps.

tashuhka
la source
1

J'écris spécifiquement pour les débutants en Python d'une manière très simple, bien qu'au fond Python fasse tellement de choses.

Commençons par le très basique:

Considérez une liste,

l = [1,2,3]

Écrivons une fonction équivalente:

def f():
    return [1,2,3]

o / p de print(l): [1,2,3]& o / p deprint(f()) : [1,2,3]

Rendons la liste l itérable: en python, la liste est toujours itérable, ce qui signifie que vous pouvez appliquer l'itérateur quand vous le souhaitez.

Appliquons l'itérateur sur la liste:

iter_l = iter(l) # iterator applied explicitly

Faisons une fonction itérable, c'est-à-dire écrivons une fonction de générateur équivalente. En python dès que vous introduisez le mot-clé yield; il devient une fonction de générateur et l'itérateur sera appliqué implicitement.

Remarque: Chaque générateur est toujours itérable avec un itérateur implicite appliqué et ici l'itérateur implicite est le nœud Donc la fonction du générateur sera:

def f():
  yield 1 
  yield 2
  yield 3

iter_f = f() # which is iter(f) as iterator is already applied implicitly

Donc si vous avez observé, dès que vous avez fait fonction générateur fa, c'est déjà iter (f)

Maintenant,

l est la liste, après application de la méthode itérateur "iter" elle devient, iter (l)

f est déjà iter (f), après avoir appliqué la méthode itérateur "iter" il devient, iter (iter (f)), qui est à nouveau iter (f)

C'est un peu que vous transformez int en int (x) qui est déjà int et il restera int (x).

Par exemple o / p de:

print(type(iter(iter(l))))

est

<class 'list_iterator'>

N'oubliez jamais que c'est Python et non C ou C ++

D'où la conclusion de l'explication ci-dessus:

liste l ~ = iter (l)

fonction générateur f == iter (f)

Jyo the Whiff
la source