Je voulais en savoir un peu plus sur iterators
, alors corrigez-moi si je me trompe.
Un itérateur est un objet qui a un pointeur sur l'objet suivant et qui est lu comme un tampon ou un flux (c'est-à-dire une liste chaînée). Ils sont particulièrement efficaces car ils ne font que vous dire ce qui va suivre par références au lieu d'utiliser l'indexation.
Cependant, je ne comprends toujours pas pourquoi le comportement suivant se produit:
In [1]: iter = (i for i in range(5))
In [2]: for _ in iter:
....: print _
....:
0
1
2
3
4
In [3]: for _ in iter:
....: print _
....:
In [4]:
Après une première boucle à travers l'itérateur (In [2]
) c'est comme s'il était consommé et laissé vide, donc la deuxième boucle (In [3]
) n'imprime rien.
Cependant, je n'ai jamais attribué de nouvelle valeur à la variable iter
.
Que se passe-t-il vraiment sous le capot de la boucle for
?
Votre suspicion est correcte: l'itérateur est consommé.
En réalité, votre itérateur est un générateur , qui est un objet qui ne peut être itéré qu'une seule fois
type((i for i in range(5))) # says it's type generator
def another_generator():
yield 1 # the yield expression makes it a generator, not a function
type(another_generator()) # also a generator
La raison pour laquelle ils sont efficaces n'a rien à voir avec le fait de vous dire ce qui va suivre "par référence". Ils sont efficaces car ils ne génèrent l'élément suivant que sur demande; tous les éléments ne sont pas générés en même temps. En fait, vous pouvez avoir un générateur infini:
def my_gen():
while True:
yield 1 # again: yield means it is a generator, not a function
for _ in my_gen(): print(_) # hit ctl+c to stop this infinite loop!
Quelques autres corrections pour aider à améliorer votre compréhension:
for
in
accepte un objet itérable comme deuxième argument.list
, ou dict
, ou un str
objet (chaîne) ou un type défini par l'utilisateur qui fournit les fonctionnalités requises.iter
est appliquée à l'objet pour obtenir un itérateur (au fait: n'utilisez pas iter
comme nom de variable en Python, comme vous ont fait - c'est l'un des mots clés). En fait, pour être plus précis, la méthode __iter__
De l'objet est appelée (ce qui est, pour la plupart, la fonction iter
de toute façon; __iter__
Est l'une des soi-disant "méthodes magiques" de Python).__iter__
Réussit, la fonction next()
est appliquée à l'objet itérable encore et encore, dans une boucle, et la première variable fourni à for
in
est affecté au résultat de la fonction next()
. (N'oubliez pas: l'objet itérable peut être un générateur, ou l'itérateur d'un objet conteneur, ou tout autre objet itérable.) En fait, pour être plus précis: il appelle l'objet __next__
!) = méthode, qui est une autre "méthode magique".for
se termine lorsque next()
lève l'exception StopIteration
(ce qui se produit généralement lorsque l'itérable n'a pas d'autre objet à céder lorsque next()
est appelée).Vous pouvez "manuellement" implémenter une boucle for
dans python de cette façon (probablement pas parfait, mais assez proche):
try:
temp = iterable.__iter__()
except AttributeError():
raise TypeError("'{}' object is not iterable".format(type(iterable).__name__))
else:
while True:
try:
_ = temp.__next__()
except StopIteration:
break
except AttributeError:
raise TypeError("iter() returned non-iterator of type '{}'".format(type(temp).__name__))
# this is the "body" of the for loop
continue
Il n'y a pratiquement pas de différence entre ce qui précède et votre exemple de code.
En fait, la partie la plus intéressante d'une boucle for
n'est pas la for
, mais la in
. L'utilisation de in
à elle seule produit un effet différent de for
in
, mais il est très utile de comprendre ce que in
fait avec ses arguments, puisque for
in
implémente un comportement très similaire.
Lorsqu'il est utilisé seul, le mot clé in
appelle d'abord la méthode __contains__
De l'objet , qui est encore une autre "méthode magique" (notez que cette étape est ignorée lorsque en utilisant for
in
). En utilisant in
seul sur un conteneur, vous pouvez faire des choses comme ceci:
1 in [1, 2, 3] # True
'He' in 'Hello' # True
3 in range(10) # True
'eH' in 'Hello'[::-1] # True
Si l'objet itérable n'est PAS un conteneur (c'est-à-dire qu'il n'a pas de méthode __contains__
), in
essaie ensuite d'appeler la méthode __iter__
De l'objet. Comme cela a été dit précédemment: la méthode __iter__
Renvoie ce qui est connu dans Python comme un itérateur . Fondamentalement, un itérateur est un objet sur lequel vous pouvez utiliser la fonction générique intégrée next()
on1. Un générateur n'est qu'un type d'itérateur.
__iter__
Réussit, le mot clé in
applique la fonction next()
à l'objet itérable à maintes reprises. (N'oubliez pas: l'objet itérable peut être un générateur, ou l'itérateur d'un objet conteneur, ou tout autre objet itérable.) En fait, pour être plus précis: il appelle l'objet __next__
!) = méthode).__iter__
Pour renvoyer un itérateur, in
puis retombe sur le protocole d'itération à l'ancienne en utilisant la méthode __getitem__
De l'objet2.TypeError
.Si vous souhaitez créer votre propre type d'objet sur lequel itérer (c'est-à-dire, vous pouvez utiliser for
in
, ou simplement in
, dessus), il est utile de connaître yield
mot clé, qui est utilisé dans générateurs (comme mentionné ci-dessus).
class MyIterable():
def __iter__(self):
yield 1
m = MyIterable()
for _ in m: print(_) # 1
1 in m # True
La présence de yield
transforme une fonction ou une méthode en générateur au lieu d'une fonction/méthode normale. Vous n'avez pas besoin de la méthode __next__
Si vous utilisez un générateur (il apporte automatiquement __next__
).
Si vous souhaitez créer votre propre type d'objet conteneur (c'est-à-dire, vous pouvez utiliser in
sur lui-même, mais PAS for
in
), vous avez juste besoin du __contains__
.
class MyUselessContainer():
def __contains__(self, obj):
return True
m = MyUselessContainer()
1 in m # True
'Foo' in m # True
TypeError in m # True
None in m # True
1 Notez que, pour être un itérateur, un objet doit implémenter le protocole itérateur . Cela signifie uniquement que les méthodes __next__
Et __iter__
Doivent être correctement mises en œuvre (les générateurs sont livrés avec cette fonctionnalité "pour gratuit ", vous n'avez donc pas à vous en préoccuper lorsque vous les utilisez). Notez également que la méthode ___next__
est en fait next
(pas de soulignement) dans Python 2 .
2 Voir cette réponse pour les différentes façons de créer des classes itérables.
La boucle for appelle essentiellement la méthode next
d'un objet qui est appliqué à (__next__
Dans Python 3).
Vous pouvez simuler cela simplement en faisant:
iter = (i for i in range(5))
print(next(iter))
print(next(iter))
print(next(iter))
print(next(iter))
print(next(iter))
# this prints 1 2 3 4
À ce stade, il n'y a pas d'élément suivant dans l'objet d'entrée. Ce faisant:
print(next(iter))
Entraînera une exception de StopIteration
levée. À ce stade, for
s'arrêtera. Et l'itérateur peut être n'importe quel objet qui répondra à la fonction next()
et lèvera l'exception quand il n'y aura plus d'éléments. Il ne doit pas s'agir d'un pointeur ou d'une référence (il n'y a pas de telles choses dans python de toute façon au sens C/C++), liste liée, etc.
Il existe un protocole d'itérateur dans python qui définit le comportement de l'instruction for
avec les listes et les dict, et d'autres choses qui peuvent être bouclées.
C'est dans le python docs ici et ici .
Le fonctionnement du protocole itérateur est généralement sous la forme d'un générateur python. Nous yield
une valeur tant que nous avons une valeur jusqu'à la fin, puis nous élevons StopIteration
Écrivons donc notre propre itérateur:
def my_iter():
yield 1
yield 2
yield 3
raise StopIteration()
for i in my_iter():
print i
Le résultat est:
1
2
3
Quelques choses à noter à ce sujet. Le my_iter est une fonction. my_iter () renvoie un itérateur.
Si j'avais plutôt écrit un itérateur comme celui-ci:
j = my_iter() #j is the iterator that my_iter() returns
for i in j:
print i #this loop runs until the iterator is exhausted
for i in j:
print i #the iterator is exhausted so we never reach this line
Et le résultat est le même que ci-dessus. L'itère est épuisé au moment où nous entrons dans la seconde boucle for.
Mais c'est plutôt simpliste, qu'en est-il de quelque chose de plus compliqué? Peut-être peut-être en boucle pourquoi pas?
def capital_iter(name):
for x in name:
yield x.upper()
raise StopIteration()
for y in capital_iter('bobert'):
print y
Et quand il s'exécute, nous utilisons l'itérateur sur le type de chaîne (qui est intégré dans iter ). À son tour, cela nous permet d'exécuter une boucle for dessus et de produire les résultats jusqu'à ce que nous ayons terminé.
B
O
B
E
R
T
Alors maintenant, cela pose la question, alors que se passe-t-il entre les rendements dans l'itérateur?
j = capital_iter("bobert")
print i.next()
print i.next()
print i.next()
print("Hey there!")
print i.next()
print i.next()
print i.next()
print i.next() #Raises StopIteration
La réponse est que la fonction est suspendue au rendement en attendant le prochain appel à next ().
B
O
B
Hey There!
E
R
T
Traceback (most recent call last):
File "", line 13, in
StopIteration
Quelques détails supplémentaires sur le comportement de iter()
avec les classes __getitem__
Qui n'ont pas leur propre méthode __iter__
.
Avant __iter__
, Il y avait __getitem__
. Si __getitem__
Fonctionne avec int
s de 0
- len(obj)-1
, alors iter()
prend en charge ces objets. Il va construire un nouvel itérateur qui appelle à plusieurs reprises __getitem__
Avec 0
, 1
, 2
, ...
Jusqu'à ce qu'il obtienne un IndexError
, qu'il convertit en StopIteration
.
Voir cette réponse pour plus de détails sur les différentes façons de créer un itérateur.
Concept 1
Tous les générateurs sont des itérateurs mais tous les itérateurs ne sont pas des générateurs
Concept 2
Un itérateur est un objet avec une méthode suivante (Python 2) ou next (Python 3).
Concept 3
Citant de wiki Generators Les fonctions Generators vous permettent de déclarer une fonction qui se comporte comme un itérateur, c'est-à-dire qu'elle peut être utilisée dans une boucle for.
Dans ton cas
>>> it = (i for i in range(5))
>>> type(it)
<type 'generator'>
>>> callable(getattr(it, 'iter', None))
False
>>> callable(getattr(it, 'next', None))
True
Extrait de le Python livre de pratique :
Nous utilisons l'instruction for pour parcourir une liste.
>>> for i in [1, 2, 3, 4]:
... print i,
...
1
2
3
4
Si nous l'utilisons avec une chaîne, il boucle sur ses caractères.
>>> for c in "python":
... print c
...
p
y
t
h
o
n
Si nous l'utilisons avec un dictionnaire, il boucle sur ses clés.
>>> for k in {"x": 1, "y": 2}:
... print k
...
y
x
Si nous l'utilisons avec un fichier, il boucle sur les lignes du fichier.
>>> for line in open("a.txt"):
... print line,
...
first line
second line
Il existe donc de nombreux types d'objets qui peuvent être utilisés avec une boucle for. Ce sont des objets itérables.
Il existe de nombreuses fonctions qui consomment ces itérables.
>>> ",".join(["a", "b", "c"])
'a,b,c'
>>> ",".join({"x": 1, "y": 2})
'y,x'
>>> list("python")
['p', 'y', 't', 'h', 'o', 'n']
>>> list({"x": 1, "y": 2})
['y', 'x']
La fonction intégrée iter prend un objet itérable et renvoie un itérateur.
>>> x = iter([1, 2, 3])
>>> x
<listiterator object at 0x1004ca850>
>>> x.next()
1
>>> x.next()
2
>>> x.next()
3
>>> x.next()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
Chaque fois que nous appelons la méthode suivante sur l'itérateur nous donne l'élément suivant. S'il n'y a plus d'éléments, il déclenche une StopIteration.
Les itérateurs sont implémentés en tant que classes. Voici un itérateur qui fonctionne comme la fonction xrange intégrée.
class yrange:
def __init__(self, n):
self.i = 0
self.n = n
def __iter__(self):
return self
def next(self):
if self.i < self.n:
i = self.i
self.i += 1
return i
else:
raise StopIteration()
La méthode iter est ce qui rend un objet itérable. Dans les coulisses, la fonction iter appelle la méthode iter sur l'objet donné.
La valeur de retour de iter est un itérateur. Il devrait avoir une méthode suivante et augmenter StopIteration lorsqu'il n'y a plus d'éléments.
Essayons-le:
>>> y = yrange(3)
>>> y.next()
0
>>> y.next()
1
>>> y.next()
2
>>> y.next()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 14, in next
De nombreuses fonctions intégrées acceptent les itérateurs comme arguments.
>>> list(yrange(5))
[0, 1, 2, 3, 4]
>>> sum(yrange(5))
10
Dans le cas ci-dessus, à la fois l'itérable et l'itérateur sont le même objet. Notez que la méthode iter a renvoyé self. Ce n'est pas toujours le cas.
class zrange:
def __init__(self, n):
self.n = n
def __iter__(self):
return zrange_iter(self.n)
class zrange_iter:
def __init__(self, n):
self.i = 0
self.n = n
def __iter__(self):
# Iterators are iterables too.
# Adding this functions to make them so.
return self
def next(self):
if self.i < self.n:
i = self.i
self.i += 1
return i
else:
raise StopIteration()
Si l'itérateur et l'itérateur sont tous deux le même objet, il est consommé en une seule itération.
>>> y = yrange(5)
>>> list(y)
[0, 1, 2, 3, 4]
>>> list(y)
[]
>>> z = zrange(5)
>>> list(z)
[0, 1, 2, 3, 4]
>>> list(z)
[0, 1, 2, 3, 4]
Les générateurs simplifient la création d'itérateurs. Un générateur est une fonction qui produit une séquence de résultats au lieu d'une seule valeur.
def yrange(n):
i = 0
while i < n:
yield i
i += 1
Chaque fois que l'instruction yield est exécutée, la fonction génère une nouvelle valeur.
>>> y = yrange(3)
>>> y
<generator object yrange at 0x401f30>
>>> y.next()
0
>>> y.next()
1
>>> y.next()
2
>>> y.next()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
Un générateur est donc également un itérateur. Vous n'avez pas à vous soucier du protocole de l'itérateur.
Le mot "générateur" est utilisé de manière confuse pour désigner à la fois la fonction qui génère et ce qu'elle génère. Dans ce chapitre, j’utiliserai le mot "générateur" pour désigner l’objet généré et "fonction générateur" pour désigner la fonction qui le génère.
Pouvez-vous penser à la façon dont cela fonctionne en interne?
Lorsqu'une fonction de générateur est appelée, elle renvoie un objet générateur sans même commencer l'exécution de la fonction. Lorsque la méthode suivante est appelée pour la première fois, la fonction commence à s'exécuter jusqu'à ce qu'elle atteigne l'instruction yield. La valeur renvoyée est renvoyée par l'appel suivant.
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
0
>>> f.next()
after yield 0
before yield 1
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
Voyons un exemple:
def integers():
"""Infinite sequence of integers."""
i = 1
while True:
yield i
i = i + 1
def squares():
for i in integers():
yield i * i
def take(n, seq):
"""Returns first n values from the given sequence."""
seq = iter(seq)
result = []
try:
for i in range(n):
result.append(seq.next())
except StopIteration:
pass
return result
print take(5, squares()) # prints [1, 4, 9, 16, 25]