Odpowiedzi:
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.
__next__
. counter
jest 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.
__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 ctr
więcej niż raz.
Counter
jest iteratorem, a iteratory powinny być iterowane tylko raz. Po zresetowaniu self.current
w __iter__
, a następnie pętla zagnieżdżona nad Counter
byłyby całkowicie uszkodzony, i wszelkiego rodzaju przyjętych zachowań iteratorów (który dzwoni iter
na nich jest idempotent) zostały naruszone. Jeśli chcesz mieć możliwość iteracji ctr
wię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.
Counter
ma być iterowalny bez iteratora, usunąłbyś definicję __next__
/ next
cał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 self
i dostępne od self
w __iter__
).
Istnieją cztery sposoby na zbudowanie funkcji iteracyjnej:
__iter__
i__next__
(lubnext
w Python 2.x))__getitem__
)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_gen
i 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 self
dział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__
.
uc_iter
powinna 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.
self.index = 0
w __iter__
tak, że można iteracyjne wiele razy. W przeciwnym razie nie możesz.
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ć.
Widzę, że niektórzy z was robi return self
w __iter__
. Chciałem tylko zauważyć, że __iter__
sam może być generatorem (eliminując w ten sposób potrzebę __next__
i podnosząc StopIteration
wyją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.
return self
w __iter__
. Kiedy chciałem yield
w nim użyć , znalazłem twój kod robiąc dokładnie to, co chcę spróbować.
next()
? return iter(self).next()
?
self.current
Ani żadnego innego licznika. To powinna być najczęściej głosowana odpowiedź!
iter
instancje klasy, ale one same nie są instancjami klasy.
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)
__len__()
metody. __getitem__
sam z oczekiwanym zachowaniem jest wystarczający.
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
, set
lub dict
, albo dowolny realizacja collections.Iterable
moż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
return iter(self.string)
.
Jest to funkcja powtarzalna bez yield
. Wykorzystuje iter
funkcję 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 nonlocal
jest 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
iter
, ale dla jasności: jest to bardziej złożone i mniej wydajne niż zwykłe korzystanie z yield
funkcji generatora; Python oferuje mnóstwo interpreterów do obsługi yield
funkcji generatora, których nie można tutaj wykorzystać, co znacznie spowalnia ten kod. Mimo to głosowano.
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]
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)