Réponses:
Les objets itérateur en python sont conformes au protocole itérateur, ce qui signifie essentiellement qu'ils fournissent deux méthodes: __iter__()
et __next__()
.
Le __iter__
renvoie l'objet itérateur et est implicitement appelé au début des boucles.
La __next__()
méthode renvoie la valeur suivante et est implicitement appelée à chaque incrément de boucle. Cette méthode déclenche une exception StopIteration lorsqu'il n'y a plus de valeur à renvoyer, qui est implicitement capturée par les constructions en boucle pour arrêter l'itération.
Voici un exemple simple de compteur:
class Counter:
def __init__(self, low, high):
self.current = low - 1
self.high = high
def __iter__(self):
return self
def __next__(self): # Python 2: def next(self)
self.current += 1
if self.current < self.high:
return self.current
raise StopIteration
for c in Counter(3, 9):
print(c)
Cela imprimera:
3
4
5
6
7
8
Il est plus facile d'écrire à l'aide d'un générateur, comme indiqué dans une réponse précédente:
def counter(low, high):
current = low
while current < high:
yield current
current += 1
for c in counter(3, 9):
print(c)
La sortie imprimée sera la même. Sous le capot, l'objet générateur prend en charge le protocole itérateur et fait quelque chose à peu près similaire à la classe Counter.
L'article de David Mertz, Iterators and Simple Generators , est une très bonne introduction.
__next__
. counter
est un itérateur, mais ce n'est pas une séquence. Il ne stocke pas ses valeurs. Par exemple, vous ne devez pas utiliser le compteur dans une boucle for doublement imbriquée.
__iter__
(en plus de in __init__
). Sinon, l'objet ne peut être itéré qu'une seule fois. Par exemple, si vous dites ctr = Counters(3, 8)
, vous ne pouvez pas utiliser for c in ctr
plus d'une fois.
Counter
est un itérateur, et les itérateurs ne sont censés être itérés qu'une seule fois. Si vous rétablissez self.current
dans __iter__
, puis une boucle sur la imbriquée Counter
serait complètement brisé, et toutes sortes de comportements présumés de itérateurs (que l' appel iter
sur eux est idempotent) ne sont pas respectés. Si vous voulez pouvoir itérer ctr
plusieurs fois, il doit s'agir d'un itérateur non itératif, où il renvoie un nouvel itérateur à chaque fois qu'il __iter__
est invoqué. Essayer de mélanger et de faire correspondre (un itérateur qui est implicitement réinitialisé lorsqu'il __iter__
est invoqué) viole les protocoles.
Counter
devait être un non-itérateur itérable, vous supprimeriez la définition de __next__
/ next
entièrement, et redéfiniriez probablement __iter__
comme une fonction de générateur de la même forme que le générateur décrit à la fin de cette réponse (sauf au lieu des limites venant d'arguments __iter__
, ils seraient arguments __init__
enregistrés sur self
et accessible à partir self
de __iter__
).
Il existe quatre façons de créer une fonction itérative:
__iter__
et__next__
(ou next
en Python 2.x))__getitem__
)Exemples:
# generator
def uc_gen(text):
for char in text.upper():
yield char
# generator expression
def uc_genexp(text):
return (char for char in text.upper())
# iterator protocol
class uc_iter():
def __init__(self, text):
self.text = text.upper()
self.index = 0
def __iter__(self):
return self
def __next__(self):
try:
result = self.text[self.index]
except IndexError:
raise StopIteration
self.index += 1
return result
# getitem method
class uc_getitem():
def __init__(self, text):
self.text = text.upper()
def __getitem__(self, index):
return self.text[index]
Pour voir les quatre méthodes en action:
for iterator in uc_gen, uc_genexp, uc_iter, uc_getitem:
for ch in iterator('abcde'):
print(ch, end=' ')
print()
Ce qui se traduit par:
A B C D E
A B C D E
A B C D E
A B C D E
Remarque :
Les deux types de générateurs ( uc_gen
et uc_genexp
) ne peuvent pas l'être reversed()
; le simple itérateur ( uc_iter
) aurait besoin de la __reversed__
méthode magique (qui, selon les documents , doit renvoyer un nouvel itérateur, mais renvoyant des self
travaux (au moins en CPython)); et le getitem iteratable ( uc_getitem
) doit avoir la __len__
méthode magique:
# for uc_iter we add __reversed__ and update __next__
def __reversed__(self):
self.index = -1
return self
def __next__(self):
try:
result = self.text[self.index]
except IndexError:
raise StopIteration
self.index += -1 if self.index < 0 else +1
return result
# for uc_getitem
def __len__(self)
return len(self.text)
Pour répondre à la question secondaire du colonel Panic sur un itérateur infiniment évalué paresseusement, voici ces exemples, en utilisant chacune des quatre méthodes ci-dessus:
# generator
def even_gen():
result = 0
while True:
yield result
result += 2
# generator expression
def even_genexp():
return (num for num in even_gen()) # or even_iter or even_getitem
# not much value under these circumstances
# iterator protocol
class even_iter():
def __init__(self):
self.value = 0
def __iter__(self):
return self
def __next__(self):
next_value = self.value
self.value += 2
return next_value
# getitem method
class even_getitem():
def __getitem__(self, index):
return index * 2
import random
for iterator in even_gen, even_genexp, even_iter, even_getitem:
limit = random.randint(15, 30)
count = 0
for even in iterator():
print even,
count += 1
if count >= limit:
break
print
Ce qui se traduit par (au moins pour ma série d'échantillons):
0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40 42 44 46 48 50 52 54
0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38
0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30
0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32
Comment choisir lequel utiliser? C'est surtout une question de goût. Les deux méthodes que je vois le plus souvent sont les générateurs et le protocole itérateur, ainsi qu'un hybride ( __iter__
renvoyant un générateur).
Les expressions de générateur sont utiles pour remplacer les compréhensions de liste (elles sont paresseuses et peuvent donc économiser des ressources).
Si l'on a besoin de compatibilité avec les versions antérieures de Python 2.x, utilisez __getitem__
.
uc_iter
devrait expirer lorsqu'elle est terminée (sinon elle le serait par infini); si vous voulez recommencer, vous devez obtenir un nouvel itérateur en appelant à uc_iter()
nouveau.
self.index = 0
en __iter__
sorte que vous pouvez itérer plusieurs fois. Sinon, vous ne pouvez pas.
Tout d'abord, le module itertools est incroyablement utile pour toutes sortes de cas dans lesquels un itérateur serait utile, mais voici tout ce dont vous avez besoin pour créer un itérateur en python:
rendement
N'est-ce pas cool? Le rendement peut être utilisé pour remplacer un retour normal dans une fonction. Il renvoie l'objet de la même manière, mais au lieu de détruire l'état et de quitter, il enregistre l'état lorsque vous souhaitez exécuter l'itération suivante. En voici un exemple en action tiré directement de la liste des fonctions itertools :
def count(n=0):
while True:
yield n
n += 1
Comme indiqué dans la description des fonctions (c'est la fonction count () du module itertools ...), il produit un itérateur qui retourne des entiers consécutifs commençant par n.
Les expressions de générateur sont un tout autre bidon de vers (vers impressionnants!). Ils peuvent être utilisés à la place d'une compréhension de liste pour économiser de la mémoire (les compréhensions de liste créent une liste en mémoire qui est détruite après utilisation si elle n'est pas affectée à une variable, mais les expressions de générateur peuvent créer un objet générateur ... qui est un moyen sophistiqué de disant Iterator). Voici un exemple de définition d'expression de générateur:
gen = (n for n in xrange(0,11))
Ceci est très similaire à notre définition d'itérateur ci-dessus, sauf que la plage complète est prédéterminée entre 0 et 10.
Je viens de trouver xrange () (surpris de ne pas l'avoir vu auparavant ...) et je l'ai ajouté à l'exemple ci-dessus. xrange () est une version itérable de range () qui a l'avantage de ne pas précompiler la liste. Ce serait très utile si vous aviez un corpus de données géant à parcourir et que vous n'aviez que trop de mémoire pour le faire.
Je vois certains d' entre vous faire return self
dans __iter__
. Je voulais juste noter que __iter__
lui - même peut être un générateur (supprimant ainsi le besoin __next__
et soulevant des StopIteration
exceptions)
class range:
def __init__(self,a,b):
self.a = a
self.b = b
def __iter__(self):
i = self.a
while i < self.b:
yield i
i+=1
Bien sûr, ici, on pourrait aussi bien faire directement un générateur, mais pour les classes plus complexes, cela peut être utile.
return self
en dedans __iter__
. Quand j'allais essayer de l'utiliser yield
, j'ai trouvé que votre code faisait exactement ce que je voulais essayer.
next()
? return iter(self).next()
?
self.current
ou tout autre compteur. Cela devrait être la meilleure réponse!
iter
instances de la classe, mais ce ne sont pas eux-mêmes des instances de la classe.
Cette question concerne les objets itérables, pas les itérateurs. En Python, les séquences sont également itérables donc une façon de faire une classe itérable est de la faire se comporter comme une séquence, c'est-à-dire de lui donner __getitem__
et des __len__
méthodes. J'ai testé cela sur Python 2 et 3.
class CustomRange:
def __init__(self, low, high):
self.low = low
self.high = high
def __getitem__(self, item):
if item >= len(self):
raise IndexError("CustomRange index out of range")
return self.low + item
def __len__(self):
return self.high - self.low
cr = CustomRange(0, 10)
for i in cr:
print(i)
__len__()
méthode. __getitem__
seul avec le comportement attendu suffit.
Toutes les réponses sur cette page sont vraiment excellentes pour un objet complexe. Mais pour ceux contenant builtin types itérables comme attributs, comme str
, list
, set
ou dict
, ou toute mise en œuvre de collections.Iterable
, vous pouvez omettre certaines choses dans votre classe.
class Test(object):
def __init__(self, string):
self.string = string
def __iter__(self):
# since your string is already iterable
return (ch for ch in self.string)
# or simply
return self.string.__iter__()
# also
return iter(self.string)
Il peut être utilisé comme:
for x in Test("abcde"):
print(x)
# prints
# a
# b
# c
# d
# e
return iter(self.string)
.
Il s'agit d'une fonction itérable sans yield
. Il utilise la iter
fonction et une fermeture qui maintient son état dans un mutable ( list
) dans la portée englobante de python 2.
def count(low, high):
counter = [0]
def tmp():
val = low + counter[0]
if val < high:
counter[0] += 1
return val
return None
return iter(tmp, None)
Pour Python 3, l'état de fermeture est conservé dans une immuable dans la portée englobante et nonlocal
est utilisé dans la portée locale pour mettre à jour la variable d'état.
def count(low, high):
counter = 0
def tmp():
nonlocal counter
val = low + counter
if val < high:
counter += 1
return val
return None
return iter(tmp, None)
Tester;
for i in count(1,10):
print(i)
1
2
3
4
5
6
7
8
9
iter
, mais juste pour être clair: c'est plus complexe et moins efficace que d'utiliser simplement une yield
fonction de générateur basé; Python a une tonne de support d'interpréteur pour les yield
fonctions de générateur basées dont vous ne pouvez pas profiter ici, ce qui rend ce code considérablement plus lent. Voté néanmoins.
Si vous cherchez quelque chose de court et de simple, cela vous suffira peut-être:
class A(object):
def __init__(self, l):
self.data = l
def __iter__(self):
return iter(self.data)
exemple d'utilisation:
In [3]: a = A([2,3,4])
In [4]: [i for i in a]
Out[4]: [2, 3, 4]
Inspiré par la réponse de Matt Gregory, voici un itérateur un peu plus compliqué qui renverra a, b, ..., z, aa, ab, ..., zz, aaa, aab, ..., zzy, zzz
class AlphaCounter:
def __init__(self, low, high):
self.current = low
self.high = high
def __iter__(self):
return self
def __next__(self): # Python 3: def __next__(self)
alpha = ' abcdefghijklmnopqrstuvwxyz'
n_current = sum([(alpha.find(self.current[x])* 26**(len(self.current)-x-1)) for x in range(len(self.current))])
n_high = sum([(alpha.find(self.high[x])* 26**(len(self.high)-x-1)) for x in range(len(self.high))])
if n_current > n_high:
raise StopIteration
else:
increment = True
ret = ''
for x in self.current[::-1]:
if 'z' == x:
if increment:
ret += 'a'
else:
ret += 'z'
else:
if increment:
ret += alpha[alpha.find(x)+1]
increment = False
else:
ret += x
if increment:
ret += 'a'
tmp = self.current
self.current = ret[::-1]
return tmp
for c in AlphaCounter('a', 'zzz'):
print(c)