Erstellen Sie einen einfachen Python-Iterator

569

Wie würde man eine iterative Funktion (oder ein Iteratorobjekt) in Python erstellen?

Akdom
quelle

Antworten:

650

Iteratorobjekte in Python entsprechen dem Iteratorprotokoll, was im Grunde bedeutet, dass sie zwei Methoden bereitstellen: __iter__() und __next__().

  • Das __iter__gibt das Iteratorobjekt zurück und wird implizit zu Beginn von Schleifen aufgerufen.

  • Die __next__()Methode gibt den nächsten Wert zurück und wird bei jedem Schleifeninkrement implizit aufgerufen. Diese Methode löst eine StopIteration-Ausnahme aus, wenn kein Wert mehr zurückgegeben werden muss, der implizit von Schleifenkonstrukten erfasst wird, um die Iteration zu stoppen.

Hier ist ein einfaches Beispiel für einen Zähler:

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)

Dies wird gedruckt:

3
4
5
6
7
8

Dies ist einfacher mit einem Generator zu schreiben, wie in einer vorherigen Antwort beschrieben:

def counter(low, high):
    current = low
    while current < high:
        yield current
        current += 1

for c in counter(3, 9):
    print(c)

Die gedruckte Ausgabe ist dieselbe. Unter der Haube unterstützt das Generatorobjekt das Iteratorprotokoll und macht etwas Ähnliches wie die Klasse Counter.

Der Artikel von David Mertz, Iteratoren und einfache Generatoren , ist eine ziemlich gute Einführung.

ars
quelle
4
Dies ist meistens eine gute Antwort, aber die Tatsache, dass es sich selbst zurückgibt, ist ein wenig suboptimal. Wenn Sie beispielsweise dasselbe Zählerobjekt in einer doppelt verschachtelten for-Schleife verwenden, erhalten Sie wahrscheinlich nicht das von Ihnen gewünschte Verhalten.
Casey Rodarmor
22
Nein, Iteratoren sollten sich selbst zurückgeben. Iterables geben Iteratoren zurück, aber iterables sollten nicht implementiert werden __next__. counterist ein Iterator, aber es ist keine Sequenz. Es speichert seine Werte nicht. Sie sollten den Zähler beispielsweise nicht in einer doppelt verschachtelten for-Schleife verwenden.
Leez
4
Im Counter-Beispiel sollte self.current in __iter__(zusätzlich zu in __init__) zugewiesen werden . Andernfalls kann das Objekt nur einmal iteriert werden. Wenn Sie beispielsweise sagen ctr = Counters(3, 8), können Sie nicht for c in ctrmehr als einmal verwenden.
Curt
7
@Curt: Auf keinen Fall. Counterist ein Iterator, und Iteratoren sollten nur einmal iteriert werden. Wenn Sie Zurücksetzen self.currentin __iter__, dann eine verschachtelte Schleife über die Countervollständig gebrochen werden würde, und alle Arten von angenommenen Verhaltensweisen von Iteratoren (das Aufruf iteran ihnen ist idempotent) verletzt werden . Wenn Sie ctrmehr als einmal iterieren möchten , muss es sich um einen nicht iterierbaren Iterator handeln, bei dem bei jedem __iter__Aufruf ein brandneuer Iterator zurückgegeben wird. Der Versuch zu mischen und abzugleichen (ein Iterator, der beim Aufrufen implizit zurückgesetzt __iter__wird) verstößt gegen die Protokolle.
ShadowRanger
2
Wenn Sie beispielsweise Counterein Nicht-Iterator iterierbar sein möchten, würden Sie die Definition von __next__/ nextvollständig entfernen und wahrscheinlich __iter__als Generatorfunktion derselben Form wie der am Ende dieser Antwort beschriebene Generator neu definieren (außer anstelle der Grenzen) Ausgehend von Argumenten bis __iter__wären dies Argumente, auf die __init__gespeichert werden soll selfund auf die von selfin zugegriffen wird __iter__.
ShadowRanger
427

Es gibt vier Möglichkeiten, eine iterative Funktion zu erstellen:

Beispiele:

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

So sehen Sie alle vier Methoden in Aktion:

for iterator in uc_gen, uc_genexp, uc_iter, uc_getitem:
    for ch in iterator('abcde'):
        print(ch, end=' ')
    print()

Was in ... resultiert:

A B C D E
A B C D E
A B C D E
A B C D E

Hinweis :

Die beiden Generatortypen ( uc_genund uc_genexp) können nicht sein reversed(); Der einfache Iterator ( uc_iter) würde die __reversed__magische Methode benötigen (die laut den Dokumenten einen neuen Iterator zurückgeben muss, aber die Rückgabe selffunktioniert (zumindest in CPython)). und das getitem iteratable ( uc_getitem) muss die __len__magische Methode haben:

    # 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)

Um die sekundäre Frage von Colonel Panic zu einem unendlich träge bewerteten Iterator zu beantworten, sind hier diese Beispiele, die jede der vier oben genannten Methoden verwenden:

# 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

Was dazu führt (zumindest für meinen Probelauf):

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

Wie wähle ich das zu verwendende aus? Dies ist meist Geschmackssache. Die beiden Methoden, die ich am häufigsten sehe, sind Generatoren und das Iteratorprotokoll sowie ein Hybrid (__iter__ Rückgabe eines Generators).

Generatorausdrücke sind nützlich, um Listenverständnisse zu ersetzen (sie sind faul und können so Ressourcen sparen).

Wenn Sie Kompatibilität mit früheren Python 2.x-Versionen benötigen, verwenden Sie __getitem__.

Ethan Furman
quelle
4
Ich mag diese Zusammenfassung, weil sie vollständig ist. Diese drei Möglichkeiten (Ausbeute, Generatorausdruck und Iterator) sind im Wesentlichen gleich, obwohl einige bequemer sind als andere. Der Ertragsoperator erfasst die "Fortsetzung", die den Status enthält (zum Beispiel den Index, den wir haben). Die Informationen werden im "Abschluss" der Fortsetzung gespeichert. Der Iterator-Weg speichert die gleichen Informationen in den Feldern des Iterators, was im Wesentlichen dasselbe ist wie ein Abschluss. Die getitem- Methode ist etwas anders, da sie in den Inhalt indiziert und nicht iterativer Natur ist.
Ian
2
@metaperl: Eigentlich ist es. In allen vier oben genannten Fällen können Sie denselben Code zum Iterieren verwenden.
Ethan Furman
1
@Asterisk: Nein, eine Instanz von uc_itersollte ablaufen, wenn sie fertig ist (andernfalls wäre sie unendlich). Wenn Sie es erneut tun möchten, müssen Sie einen neuen Iterator erhalten, indem Sie uc_iter()erneut aufrufen .
Ethan Furman
2
Sie können einstellen , self.index = 0in __iter__so dass Sie oft über laufen kann. Sonst kannst du nicht.
John Strood
1
Wenn Sie Zeit sparen könnten, würde ich mich über eine Erklärung freuen, warum Sie eine der Methoden den anderen vorziehen würden.
aaaaaa
103

Zuallererst ist das itertools-Modul unglaublich nützlich für alle Arten von Fällen, in denen ein Iterator nützlich wäre, aber hier ist alles, was Sie benötigen, um einen Iterator in Python zu erstellen:

Ausbeute

Ist das nicht cool? Die Ausbeute kann verwendet werden, um eine normale Rendite in einer Funktion zu ersetzen . Das Objekt wird trotzdem zurückgegeben, aber anstatt den Status zu zerstören und zu beenden, wird der Status gespeichert, wenn Sie die nächste Iteration ausführen möchten. Hier ist ein Beispiel dafür in Aktion, das direkt aus der Funktionsliste von itertools entnommen wurde :

def count(n=0):
    while True:
        yield n
        n += 1

Wie in der Funktionsbeschreibung angegeben (es ist die Funktion count () aus dem itertools-Modul ...), wird ein Iterator erzeugt, der aufeinanderfolgende Ganzzahlen zurückgibt, die mit n beginnen.

Generatorausdrücke sind eine ganz andere Dose Würmer (fantastische Würmer!). Sie können anstelle eines Listenverständnisses verwendet werden , um Speicherplatz zu sparen (Listenverständnisse erstellen eine Liste im Speicher, die nach der Verwendung zerstört wird, wenn sie keiner Variablen zugewiesen sind, aber Generatorausdrücke können ein Generatorobjekt erstellen ..., was eine ausgefallene Methode ist Iterator sagen). Hier ist ein Beispiel für eine Definition eines Generatorausdrucks:

gen = (n for n in xrange(0,11))

Dies ist unserer obigen Iteratordefinition sehr ähnlich, außer dass der gesamte Bereich vorgegeben ist, um zwischen 0 und 10 zu liegen.

Ich habe gerade xrange () gefunden (überrascht, dass ich es vorher noch nicht gesehen hatte ...) und es dem obigen Beispiel hinzugefügt. xrange () ist eine iterierbare Version von range () die den Vorteil hat, dass die Liste nicht vorab erstellt wird. Es wäre sehr nützlich, wenn Sie einen riesigen Datenbestand hätten, über den Sie iterieren könnten, und nur so viel Speicher hätten, um dies zu tun.

Akdom
quelle
20
Ab Python 3.0 gibt es keinen xrange () mehr und der neue range () verhält sich wie der alte xrange ()
6
Sie sollten xrange weiterhin in 2._ verwenden, da 2to3 es automatisch übersetzt.
Phob
100

Ich sehe einige von euch tun return selfin __iter__. Ich wollte nur darauf hinweisen, dass __iter__selbst ein Generator sein kann (wodurch die Notwendigkeit beseitigt __next__und StopIterationAusnahmen ausgelöst werden ).

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

Natürlich könnte man hier genauso gut direkt einen Generator bauen, aber für komplexere Klassen kann es nützlich sein.

Manux
quelle
5
Großartig! Es Schreiben so langweilig gerade return selfin __iter__. Als ich es versuchen wollte, yieldfand ich, dass Ihr Code genau das tat, was ich versuchen wollte.
Ray
3
Aber wie würde man in diesem Fall implementieren next()? return iter(self).next()?
Lenna
4
@Lenna, es ist bereits "implementiert", da iter (self) einen Iterator zurückgibt, keine Bereichsinstanz.
Manux
3
Dies ist der einfachste Weg, dies zu tun, und beinhaltet nicht, dass Sie z. B. self.currentoder einen anderen Zähler im Auge behalten müssen . Dies sollte die am besten gewählte Antwort sein!
Astrofrog
4
Um klar zu sein, macht dieser Ansatz Ihre Klasse iterierbar , aber kein Iterator . Sie erhalten jedes Mal neue Iteratoren, wenn Sie iterInstanzen der Klasse aufrufen , aber sie sind selbst keine Instanzen der Klasse.
ShadowRanger
13

Diese Frage bezieht sich auf iterierbare Objekte, nicht auf Iteratoren. In Python sind Sequenzen auch iterierbar. Eine Möglichkeit, eine iterierbare Klasse zu erstellen, besteht darin, sie wie eine Sequenz zu verhalten, dh sie __getitem__und __len__Methoden anzugeben. Ich habe dies auf Python 2 und 3 getestet.

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
quelle
1
Es muss keine __len__()Methode haben. __getitem__allein mit dem erwarteten Verhalten ist ausreichend.
BlackJack
5

Alle Antworten auf dieser Seite eignen sich hervorragend für ein komplexes Objekt. Aber für jene , die builtin iterable Typen als Attribut, wie str, list, setoder dict, oder jede Implementierung collections.Iterablekönnen Sie bestimmte Dinge in der Klasse weglassen.

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)

Es kann verwendet werden wie:

for x in Test("abcde"):
    print(x)

# prints
# a
# b
# c
# d
# e
John Strood
quelle
1
Wie Sie sagten, ist die Zeichenfolge bereits iterierbar. Warum also der zusätzliche Generatorausdruck dazwischen, anstatt nur die Zeichenfolge nach dem Iterator zu fragen (was der Generatorausdruck intern tut) : return iter(self.string).
BlackJack
@ BlackJack Du hast in der Tat recht. Ich weiß nicht, was mich dazu bewogen hat, so zu schreiben. Vielleicht habe ich versucht, Verwirrung in einer Antwort zu vermeiden, um die Funktionsweise der Iteratorsyntax in Form von mehr Iteratorsyntax zu erklären.
John Strood
3

Dies ist eine iterierbare Funktion ohne yield. Es nutzt die iterFunktion und einen Abschluss, der seinen Status in einem veränderlichen ( list) im umschließenden Bereich für Python 2 hält.

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)

Bei Python 3 wird der Abschlussstatus im umschließenden Bereich unveränderlich gehalten und nonlocalim lokalen Bereich zum Aktualisieren der Statusvariablen verwendet.

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)  

Prüfung;

for i in count(1,10):
    print(i)
1
2
3
4
5
6
7
8
9
Nizam Mohamed
quelle
Ich schätze immer eine clevere Verwendung von zwei Argumenten iter, aber um ganz klar zu sein: Dies ist komplexer und weniger effizient als nur die Verwendung einer yieldbasierten Generatorfunktion; Python bietet eine Menge Interpreter-Unterstützung für yieldbasierte Generatorfunktionen, die Sie hier nicht nutzen können, wodurch dieser Code erheblich langsamer wird. Trotzdem hochgestimmt.
ShadowRanger
2

Wenn Sie nach etwas Kurzem und Einfachem suchen, reicht es Ihnen vielleicht:

class A(object):
    def __init__(self, l):
        self.data = l

    def __iter__(self):
        return iter(self.data)

Anwendungsbeispiel:

In [3]: a = A([2,3,4])

In [4]: [i for i in a]
Out[4]: [2, 3, 4]
Daniil Mashkin
quelle
-1

Inspiriert von Matt Gregorys Antwort ist hier ein etwas komplizierterer Iterator, der a, b, ..., z, aa, ab, ..., zz, aaa, aab, ..., zzy, zzz zurückgibt

    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
quelle