Zbuduj podstawowy iterator w języku Python


Odpowiedzi:


649

Obiekty iteratora w pythonie są zgodne z protokołem iteratora, co w zasadzie oznacza, że ​​zapewniają dwie metody: __iter__() i __next__().

  • __iter__Zwraca obiekt iteracyjnej i nazywany jest niejawnie na początku pętli.

  • __next__()Sposób powraca następną wartość zwana jest pośrednio na każde kolejne pętli. Ta metoda wywołuje wyjątek StopIteration, gdy nie ma już wartości do zwrócenia, która jest domyślnie przechwytywana przez zapętlone konstrukcje, aby zatrzymać iterację.

Oto prosty przykład licznika:

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)

Spowoduje to wydrukowanie:

3
4
5
6
7
8

Łatwiej jest pisać za pomocą generatora, jak opisano w poprzedniej odpowiedzi:

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

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

Wydruk będzie taki sam. Pod maską obiekt generatora obsługuje protokół iteratora i robi coś mniej więcej podobnego do klasy Counter.

Artykuł Davida Mertza, Iterators and Simple Generators , jest całkiem dobrym wstępem.


4
Jest to w większości dobra odpowiedź, ale fakt, że zwraca siebie, jest nieco nieoptymalny. Na przykład, jeśli użyjesz tego samego obiektu licznika w podwójnie zagnieżdżonej pętli for, prawdopodobnie nie uzyskasz tego, co miałeś na myśli.
Casey Rodarmor

22
Nie, iteratory POWINNY się zwrócić. Iterables zwracają iteratory, ale iterable nie powinny się implementować __next__. counterjest iteratorem, ale nie jest sekwencją. Nie przechowuje swoich wartości. Na przykład nie powinieneś używać licznika w podwójnie zagnieżdżonej pętli for.
leewz

4
W przykładzie Counter self.current powinien być przypisany w __iter__(oprócz in __init__). W przeciwnym razie obiekt można powtórzyć tylko raz. Na przykład, jeśli powiesz ctr = Counters(3, 8), nie możesz użyć for c in ctrwięcej niż raz.
Curt

7
@Curt: Absolutnie nie. Counterjest iteratorem, a iteratory powinny być iterowane tylko raz. Po zresetowaniu self.currentw __iter__, a następnie pętla zagnieżdżona nad Counterbyłyby całkowicie uszkodzony, i wszelkiego rodzaju przyjętych zachowań iteratorów (który dzwoni iterna nich jest idempotent) zostały naruszone. Jeśli chcesz mieć możliwość iteracji ctrwięcej niż jeden raz, musi to być iterator bez iteracji, w którym zwraca za każdym razem zupełnie nowy iterator __iter__. Próba mieszania i dopasowywania (iterator, który jest domyślnie resetowany po __iter__wywołaniu) narusza protokoły.
ShadowRanger

2
Na przykład, jeśli Counterma być iterowalny bez iteratora, usunąłbyś definicję __next__/ nextcałkowicie i prawdopodobnie przedefiniowałbyś __iter__funkcję generatora o tej samej formie co generator opisany na końcu tej odpowiedzi (z wyjątkiem zamiast granic pochodzących z argumentów __iter__, że będą argumenty __init__zapisywane na selfi dostępne od selfw __iter__).
ShadowRanger

427

Istnieją cztery sposoby na zbudowanie funkcji iteracyjnej:

Przykłady:

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

Aby zobaczyć wszystkie cztery metody w akcji:

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

Co skutkuje w:

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

Uwaga :

Dwa typy generatorów ( uc_geni uc_genexp) nie mogą być reversed(); zwykły iterator ( uc_iter) potrzebowałby __reversed__magicznej metody (która, zgodnie z dokumentacją , musi zwrócić nowy iterator, ale zwracanie selfdziała (przynajmniej w CPython)); a getitem iteratable ( uc_getitem) musi mieć __len__metodę magiczną:

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

Aby odpowiedzieć na drugorzędne pytanie pułkownika Paniki dotyczące nieskończonego, leniwie ocenianego iteratora, oto te przykłady, wykorzystujące każdą z czterech powyższych metod:

# 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

Które wyniki (przynajmniej dla mojej próbki):

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

Jak wybrać, którego użyć? Jest to głównie kwestia gustu. Dwie najczęściej spotykane metody to generatory i protokół iteratora, a także hybryda ( __iter__zwracanie generatora).

Wyrażenia generatora są przydatne do zastępowania wyrażeń listowych (są leniwe i mogą oszczędzać zasoby).

Jeśli potrzebna jest kompatybilność z wcześniejszymi wersjami Python 2.x użyj __getitem__.


4
Lubię to streszczenie, ponieważ jest kompletne. Te trzy sposoby (wydajność, wyrażenie generatora i iterator) są zasadniczo takie same, chociaż niektóre są wygodniejsze niż inne. Operator zbiorów przechwytuje „kontynuację”, która zawiera stan (na przykład indeks, do którego zmierzamy). Informacje są zapisywane w „zamknięciu” kontynuacji. Sposób iteratora zapisuje te same informacje w polach iteratora, co jest zasadniczo tym samym co zamknięcie. Metoda getitem jest nieco inna, ponieważ indeksuje do zawartości i nie ma charakteru iteracyjnego.
Ian

2
@metaperl: Właściwie to jest. We wszystkich czterech powyższych przypadkach możesz użyć tego samego kodu do iteracji.
Ethan Furman,

1
@ Gwiazdka: Nie, instancja uc_iterpowinna wygasnąć po jej zakończeniu (w przeciwnym razie nastąpiłoby to w nieskończoność); jeśli chcesz to zrobić ponownie, musisz uzyskać nowy iterator, dzwoniąc uc_iter()ponownie.
Ethan Furman,

2
Można ustawić self.index = 0w __iter__tak, że można iteracyjne wiele razy. W przeciwnym razie nie możesz.
John Strood,

1
Jeśli mógłbyś poświęcić czas, byłbym wdzięczny za wyjaśnienie, dlaczego wybrałeś którąś z metod spośród innych.
aaaaaa

103

Przede wszystkim moduł itertools jest niezwykle przydatny w różnego rodzaju przypadkach, w których przydatny byłby iterator, ale oto wszystko, czego potrzebujesz, aby utworzyć iterator w pythonie:

wydajność

Czy to nie fajne? Wydajność może być wykorzystana do zastąpienia normalnego powrotu w funkcji. Zwraca obiekt tak samo, ale zamiast niszczyć stan i wychodzić, zapisuje stan na wypadek, gdy chcesz wykonać następną iterację. Oto przykład tego działania pobranego bezpośrednio z listy funkcji itertools :

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

Jak podano w opisie funkcji (jest to funkcja count () z modułu itertools ...), tworzy iterator, który zwraca kolejne liczby całkowite zaczynające się od n.

Wyrażenia generatora to zupełnie inna puszka robaków (niesamowite robaki!). Mogą być używane zamiast Zrozumienia listy w celu oszczędzania pamięci (wyrazy z listy tworzą listę w pamięci, która ulega zniszczeniu po użyciu, jeśli nie jest przypisana do zmiennej, ale wyrażenia generatora mogą tworzyć Obiekt Generatora ... co jest fantazyjnym sposobem mówiąc Iterator). Oto przykład definicji wyrażenia generatora:

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

Jest to bardzo podobne do powyższej definicji iteratora, z tym wyjątkiem, że pełny zakres jest z góry określony między 0 a 10.

Właśnie znalazłem xrange () (zaskoczony, że nie widziałem go wcześniej ...) i dodałem go do powyższego przykładu. xrange () jest iterowalną wersją range (), która ma tę zaletę, że nie buduje listy wcześniej. Byłoby bardzo przydatne, gdybyś miał gigantyczny zbiór danych do iteracji i miał tylko tyle pamięci, aby to zrobić.


20
od wersji Python 3.0 nie ma już xrange (), a nowy zakres () zachowuje się jak stary xrange ()

6
Nadal powinieneś używać xrange w 2._, ponieważ 2to3 tłumaczy to automatycznie.
Phob

100

Widzę, że niektórzy z was robi return selfw __iter__. Chciałem tylko zauważyć, że __iter__sam może być generatorem (eliminując w ten sposób potrzebę __next__i podnosząc StopIterationwyjątki)

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

Oczywiście tutaj równie dobrze można stworzyć generator, ale w przypadku bardziej złożonych klas może być on użyteczny.


5
Świetny! To tak nudne pisać tylko return selfw __iter__. Kiedy chciałem yieldw nim użyć , znalazłem twój kod robiąc dokładnie to, co chcę spróbować.
Ray

3
Ale w tym przypadku, jak można to wdrożyć next()? return iter(self).next()?
Lenna

4
@Lenna, jest już „zaimplementowane”, ponieważ iter (self) zwraca iterator, a nie instancję zakresu.
Manux,

3
Jest to najłatwiejszy sposób na zrobienie tego i nie wymaga śledzenia np. self.currentAni żadnego innego licznika. To powinna być najczęściej głosowana odpowiedź!
astrofrog

4
Mówiąc wprost, takie podejście sprawia, że ​​twoja klasa jest iterowalna , ale nie iteracyjna . Otrzymujesz świeże iteratory za każdym razem, gdy wywołujesz iterinstancje klasy, ale one same nie są instancjami klasy.
ShadowRanger

13

To pytanie dotyczy obiektów iterowalnych, a nie iteratorów. W Pythonie sekwencje też są iterowalne, więc jednym ze sposobów na stworzenie klasy iterowalnej jest sprawienie, aby zachowywała się jak sekwencja, tj. Podanie jej __getitem__i __len__metod. Przetestowałem to na Python 2 i 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)

1
To nie musi mieć __len__()metody. __getitem__sam z oczekiwanym zachowaniem jest wystarczający.
BlackJack

5

Wszystkie odpowiedzi na tej stronie są naprawdę świetne dla złożonego obiektu. Ale dla tych, które zawierają wbudowane iterowalny typów jako atrybuty, takie jak str, list, setlub dict, albo dowolny realizacja collections.Iterablemożna pominąć pewne rzeczy w swojej klasie.

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)

Może być używany jak:

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

# prints
# a
# b
# c
# d
# e

1
Jak pan powiedział, ciąg jest już iterable więc dlaczego dodatkowo wyrażenie generator pomiędzy zamiast po prostu pytając ciąg dla iteratora (którego ekspresja generator robi wewnętrznie) return iter(self.string).
BlackJack

@BlackJack Rzeczywiście masz rację. Nie wiem, co skłoniło mnie do napisania w ten sposób. Być może starałem się uniknąć nieporozumień w odpowiedzi, próbując wyjaśnić działanie składni iteratora w kategoriach większej składni iteratora.
John Strood

3

Jest to funkcja powtarzalna bez yield. Wykorzystuje iterfunkcję i zamknięcie, które utrzymuje jej stan w zmiennej ( list) w zakresie obejmującym 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)

W Pythonie 3 stan zamknięcia jest niezmienny w zakresie obejmującym i nonlocaljest używany w zasięgu lokalnym do aktualizacji zmiennej stanu.

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

Zawsze doceniam sprytne użycie dwóch argumentów iter, ale dla jasności: jest to bardziej złożone i mniej wydajne niż zwykłe korzystanie z yieldfunkcji generatora; Python oferuje mnóstwo interpreterów do obsługi yieldfunkcji generatora, których nie można tutaj wykorzystać, co znacznie spowalnia ten kod. Mimo to głosowano.
ShadowRanger

2

Jeśli szukasz czegoś krótkiego i prostego, może ci to wystarczy:

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

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

przykład użycia:

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

In [4]: [i for i in a]
Out[4]: [2, 3, 4]

-1

Zainspirowany odpowiedzią Matt Gregory tutaj jest nieco bardziej skomplikowany iterator, który zwróci 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)
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.