Construire un itérateur Python de base

569

Comment créer une fonction itérative (ou objet itérateur) en python?

akdom
la source

Réponses:

650

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.

ars
la source
4
C'est surtout une bonne réponse, mais le fait qu'il renvoie self est un peu sous-optimal. Par exemple, si vous utilisiez le même objet compteur dans une boucle for imbriquée pour deux fois, vous n'auriez probablement pas le comportement que vous vouliez.
Casey Rodarmor
22
Non, les itérateurs DEVRAIENT se retourner. Les itérables renvoient des itérateurs, mais les itérables ne doivent pas être implémentés __next__. counterest 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.
leewz
4
Dans l'exemple Counter, self.current doit être affecté à __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 ctrplus d'une fois.
Curt
7
@Curt: Absolument pas. Counterest un itérateur, et les itérateurs ne sont censés être itérés qu'une seule fois. Si vous rétablissez self.currentdans __iter__, puis une boucle sur la imbriquée Counterserait complètement brisé, et toutes sortes de comportements présumés de itérateurs (que l' appel itersur eux est idempotent) ne sont pas respectés. Si vous voulez pouvoir itérer ctrplusieurs 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.
ShadowRanger
2
Par exemple, si Counterdevait être un non-itérateur itérable, vous supprimeriez la définition de __next__/ nextentiè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 selfet accessible à partir selfde __iter__).
ShadowRanger
427

Il existe quatre façons de créer une fonction itérative:

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_genet 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 selftravaux (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__.

Ethan Furman
la source
4
J'aime ce résumé car il est complet. Ces trois façons (rendement, expression de générateur et itérateur) sont essentiellement les mêmes, bien que certaines soient plus pratiques que d'autres. L'opérateur de rendement capture la "continuation" qui contient l'état (par exemple l'indice auquel nous nous adressons). Les informations sont enregistrées dans la "clôture" de la suite. L'itérateur enregistre les mêmes informations dans les champs de l'itérateur, ce qui est essentiellement la même chose qu'une fermeture. La méthode getitem est un peu différente car elle indexe dans le contenu et n'est pas de nature itérative.
Ian
2
@metaperl: En fait, ça l'est. Dans les quatre cas ci-dessus, vous pouvez utiliser le même code pour itérer.
Ethan Furman
1
@ Astérisque: Non, une instance de uc_iterdevrait 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.
Ethan Furman
2
Vous pouvez régler self.index = 0en __iter__sorte que vous pouvez itérer plusieurs fois. Sinon, vous ne pouvez pas.
John Strood
1
Si vous pouviez gagner du temps, j'apprécierais une explication pour laquelle vous choisiriez l'une des méthodes plutôt que les autres.
aaaaaa
103

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.

akdom
la source
20
depuis python 3.0, il n'y a plus de xrange () et la nouvelle gamme () se comporte comme l'ancienne xrange ()
6
Vous devez toujours utiliser xrange dans 2._, car 2to3 le traduit automatiquement.
Phob
100

Je vois certains d' entre vous faire return selfdans __iter__. Je voulais juste noter que __iter__lui - même peut être un générateur (supprimant ainsi le besoin __next__et soulevant des StopIterationexceptions)

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.

Manux
la source
5
Génial! C'est tellement ennuyeux d'écrire juste return selfen dedans __iter__. Quand j'allais essayer de l'utiliser yield, j'ai trouvé que votre code faisait exactement ce que je voulais essayer.
Ray
3
Mais dans ce cas, comment mettre en œuvre next()? return iter(self).next()?
Lenna
4
@Lenna, il est déjà "implémenté" car iter (self) retourne un itérateur, pas une instance de plage.
Manux
3
C'est la façon la plus simple de le faire, et cela n'implique pas d'avoir à suivre par exemple self.currentou tout autre compteur. Cela devrait être la meilleure réponse!
astrofrog
4
Pour être clair, cette approche rend votre classe itérable , mais pas un itérateur . Vous obtenez de nouveaux itérateurs à chaque fois que vous appelez des iterinstances de la classe, mais ce ne sont pas eux-mêmes des instances de la classe.
ShadowRanger
13

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)
aq2
la source
1
Il n'a pas besoin d'avoir de __len__()méthode. __getitem__seul avec le comportement attendu suffit.
BlackJack
5

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, setou 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
John Strood
la source
1
Comme vous l' avez dit, la chaîne est déjà itérables alors pourquoi l'expression du générateur supplémentaire entre au lieu de simplement demander à la chaîne pour la iterator (que l'expression du générateur interne ne): return iter(self.string).
BlackJack
@BlackJack Vous avez en effet raison. Je ne sais pas ce qui m'a persuadé d'écrire de cette façon. J'essayais peut-être d'éviter toute confusion dans une réponse en essayant d'expliquer le fonctionnement de la syntaxe de l'itérateur en termes de syntaxe plus itérative.
John Strood
3

Il s'agit d'une fonction itérable sans yield. Il utilise la iterfonction 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 nonlocalest 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
Nizam Mohamed
la source
J'apprécie toujours une utilisation intelligente de deux arguments iter, mais juste pour être clair: c'est plus complexe et moins efficace que d'utiliser simplement une yieldfonction de générateur basé; Python a une tonne de support d'interpréteur pour les yieldfonctions 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.
ShadowRanger
2

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]
Daniil Mashkin
la source
-1

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)
Ace.Di
la source