2008-08-21 35 views
427

W jaki sposób utworzyć funkcję iteracyjną (lub obiekt iteracyjny) w pythonie?Tworzenie podstawowego Iteratora Pythona

+3

Tutaj są dwa pytania, oba ważne. Jak sprawić, by klasa była iterowalna (tzn. I jak utworzyć funkcję, która zwraca sekwencję z leniwą oceną? –

+4

Dobre ćwiczenie myślę, że napisać klasę, która reprezentuje liczby parzyste (nieskończona sekwencja). –

+1

@ColonelPanic: Okay, dodano przykład nieskończonej liczby do [moja odpowiedź] (http://stackoverflow.com/a/7542261/208880). –

Odpowiedz

499

Obiekty Iteratora w pythonie są zgodne z protokołem iteratora, co w zasadzie oznacza, że ​​zapewniają dwie metody: __iter__() i next(). __iter__ zwraca obiekt iteratora i jest niejawnie wywoływany na początku pętli. Metoda next() zwraca następną wartość i jest niejawnie wywoływana dla każdego przyrostu pętli. next() podnosi wyjątek StopIteration, gdy nie ma więcej wartości do zwrócenia, która jest niejawnie przechwytywana przez zapętlenie konstrukcji w celu zatrzymania iteracji.

Oto prosty przykład licznika:

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 

ten wypisze:

3 
4 
5 
6 
7 
8 

Łatwiej to napisać przy użyciu generatora, objętych w poprzedniej odpowiedzi:

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

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

Wydrukowane wydruki będą takie same. Pod maską obiekt generatora obsługuje protokół iteratora i robi coś z grubsza podobnego do klasy Counter.

Artykuł Davida Mertza, Iterators and Simple Generators, jest całkiem niezłym wprowadzeniem.

+45

Zauważ, że funkcja 'next()' nie ma wartości 'yield',' zwraca 'je. –

+46

To jest nieprawidłowe w Pythonie 3 --- musi to być '__next __()'. – Aerovistae

+2

Jest to w większości dobra odpowiedź, ale fakt, że wraca on sam, jest trochę mniej optymalny. Na przykład, jeśli użyjesz tego samego obiektu licznika w podwójnie zagnieżdżonej pętli for, prawdopodobnie nie otrzymasz takiego zachowania, jakie miałeś na myśli. –

97

Przede wszystkim itertools module jest niezwykle przydatna dla wszystkich rodzajów spraw, w których iterator byłyby użyteczne, ale tutaj jest wszystko, czego potrzeba, aby utworzyć iteracyjnej w python:

wydajność

Czy to nie jest fajne? Wydajność może być użyta w celu zastąpienia zwykłej funkcji powrotu. Zwraca obiekt tak samo, ale zamiast niszczyć stan i wychodzić, zapisuje stan, gdy chcesz wykonać następną iterację. Oto przykład jak to działa wyciągnął bezpośrednio z itertools function list:

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

Jak podano w opisie funkcji (jest to count() funkcji z modułu itertools ...), to powoduje, że iterator zwraca kolejne liczby całkowite zaczynające się od n.

Generator expressions to cała inna puszka robaków (niesamowite robale!). Mogą być użyte zamiast List Comprehension, aby zaoszczędzić pamięć (spisane listy tworzą listę w pamięci, która jest zniszczona po użyciu, jeśli nie jest przypisana do zmiennej, ale wyrazy generatora mogą tworzyć obiekt generatora ... który jest fantazyjnym sposobem powiedzenia Iterator). Oto przykład określenia ekspresji generatora:

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

ta jest bardzo podobna do iteracyjnej naszą definicją powyżej, z wyjątkiem pełnego zakresu jest ustalona na poziomie między 0 a 10.

właśnie się XRange() (zaskoczony, że nie widziałem go wcześniej ...) i dodałem go do powyższego przykładu. xrange() to iterowalna wersja zakresu (), która ma tę zaletę, że nie jest przygotowana do utworzenia listy. Byłoby bardzo przydatne, gdybyś miał gigantyczny korpus danych do iteracji i miał tylko tyle pamięci, aby to zrobić.

+17

od python 3.0 nie ma już xrange() i nowy range() zachowuje się jak stary xrange() – hop

+6

Powinieneś używać xrange w 2._, ponieważ 2to3 tłumaczy to automatycznie. – Phob

305

Istnieją cztery sposoby tworzenia iteracyjne funkcję:

Przykłady:

# 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 

żeby zobaczyć wszystkie cztery metody w akcji:

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

co skutkuje:

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

Uwaga:

Dwa generatora typy (uc_gen i uc_genexp) nie mogą być reversed(); zwykły iterator (uc_iter) wymagałby magicznej metody (która musi zwracać nowy iterator, który cofa się); a GetItem iteracyjnych (uc_getitem) musi mieć metodę __len__ Magic:

# for uc_iter 
    def __reversed__(self): 
     return reversed(self.text) 

    # for uc_getitem 
    def __len__(self) 
     return len(self.text) 

Aby odpowiedzieć na pytanie wtórnego pułkownika panika jest o nieskończonej leniwie ocenianego iterator, tutaj są te przykłady, używając każdej 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 

co powoduje (przynajmniej dla mojego biegu próbek):

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 
+4

Podoba mi się to podsumowanie, ponieważ jest kompletne. Te trzy sposoby (wydajność, ekspresja generatora i iterator) są zasadniczo takie same, chociaż niektóre są wygodniejsze niż inne. Operator yield przechwytuje "kontynuację", która zawiera stan (na przykład indeks, do którego jesteśmy uprawnieni). Informacje są zapisywane w "zamknięciu" kontynuacji. Sposób iteracji zapisuje te same informacje w polach iteratora, który jest w zasadzie tym samym co zamknięcie. Metoda __getitem__ jest trochę inna, ponieważ indeksuje zawartość i nie ma charakteru iteracyjnego. – Ian

+0

Nie zwiększasz indeksu w twoim ostatnim podejściu, 'uc_getitem()'.Właściwie podczas refleksji nie powinien zwiększać indeksu, ponieważ go nie obsługuje. Ale to także nie jest sposób na abstrakcyjną iterację. –

+2

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

79

Widzę niektórych z was robi return self w __iter__. Chciałem tylko zwrócić uwagę, że sama __iter__ może być generator (usuwając w ten sposób potrzebę __next__ i podnoszenie StopIteration wyjątkami)

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 jeden równie dobrze może bezpośrednio dokonać generator, ale w przypadku bardziej skomplikowanych klas to możliwe być przydatnym.

+5

Świetnie! To takie nudne pisanie właśnie 'return self' w' __iter__'. Kiedy miałem zamiar spróbować użyć 'yield' w tym pliku znalazłem twój kod robiący dokładnie to, co chcę wypróbować. – Ray

+2

Ale w takim przypadku, w jaki sposób można wdrożyć 'next()'? 'return iter (self) .next()'? – Lenna

+4

@Lenna, jest już "zaimplementowana", ponieważ iter (self) zwraca iterator, a nie instancję zasięgu. – Manux

3

Jest to funkcja iteracyjna bez yield. To sprawi, że korzystanie z funkcji iter i zamknięcia, który utrzymuje, że to państwo w sposób zmienny (list) w zamykającym zakres 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) 

dla Pythona 3, stan zamknięcia jest przechowywany w niezmiennym w zakresie osłaniającego i nonlocal jest używany w zakresie lokalnym, aby zaktualizować zmienną 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 
+0

Zawsze doceniam sprytne użycie dwóch argumentów 'iter', ale dla jasności: jest to bardziej złożone i mniej wydajne niż użycie funkcji generującej opartej na" wydajności "; Python ma mnóstwo interpreterów wspierających funkcje generujące 'yield', których nie można tu wykorzystać, co znacznie spowalnia ten kod. Wciąż przegłosowywany. – ShadowRanger

7

To pytanie dotyczy obiektów iteracyjnych, a nie iteratorów. W języku Python sekwencje są również iterowalne, więc jednym ze sposobów utworzenia klasy iterowalnej jest sprawienie, aby zachowywała się jak sekwencja, tj. Nadawanie jej metod. Przetestowałem to na Pythonie 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)