Construire un itérateur Python de base
Question
Comment créer une fonction itérative (ou un objet itérateur) en python ?
La solution
Les objets itérateurs 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.Le next()
La méthode renvoie la valeur suivante et est implicitement appelée à chaque incrément de boucle. next()
lève 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
self.high = high
def __iter__(self):
return self
def next(self): # Python 3: def __next__(self)
if self.current > self.high:
raise StopIteration
else:
self.current += 1
return self.current - 1
for c in Counter(3, 8):
print c
Cela imprimera :
3
4
5
6
7
8
C'est plus facile à é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, 8):
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, Itérateurs et générateurs simples, est une très bonne introduction.
Autres conseils
Il existe quatre façons de créer une fonction itérative :
- créer un générateur (utilise le mot-clé de rendement)
- utilisez une expression génératrice (genexp)
- créer un itérateur (définit
__iter__
et__next__
(ounext
en Python 2.x)) - créer une classe sur laquelle Python peut parcourir tout seul (définit
__getitem__
)
Exemples:
# generator
def uc_gen(text):
for char in text:
yield char.upper()
# generator expression
def uc_genexp(text):
return (char.upper() for char in text)
# iterator protocol
class uc_iter():
def __init__(self, text):
self.text = text
self.index = 0
def __iter__(self):
return self
def __next__(self):
try:
result = self.text[self.index].upper()
except IndexError:
raise StopIteration
self.index += 1
return result
# getitem method
class uc_getitem():
def __init__(self, text):
self.text = text
def __getitem__(self, index):
result = self.text[index].upper()
return result
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,
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
Note:
Les deux types de générateurs (uc_gen
et uc_genexp
) c'est pas possible reversed()
;l'itérateur simple (uc_iter
) aurait besoin du __reversed__
méthode magique (qui doit renvoyer un nouvel itérateur qui va en arrière) ;et le getitem itérable (uc_getitem
) doit avoir le __len__
méthode magique :
# for uc_iter
def __reversed__(self):
return reversed(self.text)
# for uc_getitem
def __len__(self)
return len(self.text)
Pour répondre à la question secondaire du colonel Panic sur un itérateur infini é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 donne (au moins pour mon exemple d'exécution) :
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
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 rendement normal retour 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 du liste des fonctions itertools:
def count(n=0):
while True:
yield n
n += 1
Comme indiqué dans la description des fonctions (c'est le compter() fonction du module itertools...), il produit un itérateur qui renvoie des entiers consécutifs commençant par n.
Expressions génératrices sont une toute autre boîte de Pandore (des vers géniaux !).Ils peuvent être utilisés à la place d'un Compréhension de la 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 génératrices peuvent créer un objet générateur...ce qui est une façon élégante de dire Iterator).Voici un exemple de définition d'expression génératrice :
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 plagex() (surpris de ne pas l'avoir vu auparavant...) et je l'ai ajouté à l'exemple ci-dessus. plagex() est une version itérable de gamme() ce qui a l'avantage de ne pas préconstruire la liste.Ce serait très utile si vous disposiez d'un corpus géant de données sur lequel parcourir et d'une quantité limitée 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 (éliminant ainsi le besoin de __next__
et élever StopIteration
des 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 autant faire directement un générateur, mais pour des classes plus complexes cela peut être utile.
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 créer une classe itérable est de la faire se comporter comme une séquence, c'est-à-diredonne-le __getitem__
et __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)
Il s'agit d'une fonction itérable sans yield
.Il utilise le iter
fonction et une fermeture qui maintient son état dans un état 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 un état 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)
Test;
for i in count(1,10):
print(i)
1
2
3
4
5
6
7
8
9
Toutes les réponses sur cette page sont vraiment idéales pour un objet complexe.Mais pour ceux qui contiennent des types itérables intégrés 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 string)
Il peut être utilisé comme :
for x in Test("abcde"):
print(x)
# prints
# a
# b
# c
# d
# e
Si vous cherchez quelque chose de court et 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)