web-dev-qa-db-fra.com

Python pour le comportement des boucles et des itérateurs

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?

42
Matteo

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:

  • Le générateur n'est pas un pointeur et ne se comporte pas comme un pointeur comme vous le savez peut-être dans d'autres langues.
  • L'une des différences par rapport aux autres langues: comme indiqué ci-dessus, chaque résultat du générateur est généré à la volée. Le résultat suivant n'est pas produit tant qu'il n'est pas demandé.
  • La combinaison de mots clés forin accepte un objet itérable comme deuxième argument.
  • L'objet itérable peut être un générateur, comme dans votre cas d'exemple, mais il peut également être tout autre objet itérable, tel qu'un list, ou dict, ou un str objet (chaîne) ou un type défini par l'utilisateur qui fournit les fonctionnalités requises.
  • La fonction 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).
  • Si l'appel à __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 à forin 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".
  • La boucle 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 forin, mais il est très utile de comprendre ce que in fait avec ses arguments, puisque forin 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 forin). 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.

  • Si l'appel à __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).
  • Si l'objet n'a pas de 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.
  • Si toutes les tentatives ci-dessus échouent, vous obtiendrez une exception TypeError .

Si vous souhaitez créer votre propre type d'objet sur lequel itérer (c'est-à-dire, vous pouvez utiliser forin, 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 forin), 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.

53
Rick Teachey

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.

18
Marcin

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

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

4
Ethan Furman

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

Extrait de le Python livre de pratique :


5. Itérateurs et générateurs

5.1. Itérateurs

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

5.1.1. Le protocole d'itération

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>

StopIteration

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

StopIteration

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]

5.2. Générateurs

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>

StopIteration

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]
2
drewteriyaki