





Прежде всего, модуль itertools невероятно полезен для всех видов случаев, в которых был бы полезен итератор, но вот все, что вам нужно для создания итератора в python:
yield
Разве это не круто? Доходность может использоваться для замены обычного возвращаться в функции. Он возвращает объект точно так же, но вместо разрушения состояния и выхода сохраняет состояние, когда вы хотите выполнить следующую итерацию. Вот пример этого в действии, извлеченный непосредственно из список функций itertools:
def count(n=0):
while True:
yield n
n += 1
Как указано в описании функций (это функция считать() из модуля itertools ...), он создает итератор, который возвращает последовательные целые числа, начинающиеся с n.
Генератор выражений - это совершенно другая баня червей (крутые черви!). Они могут использоваться вместо Понимание списка для экономии памяти (понимание списков создает список в памяти, который уничтожается после использования, если не назначен переменной, но выражения генератора могут создавать объект генератора ... что является причудливым способом сказать Итератор). Вот пример определения выражения генератора:
gen = (n for n in xrange(0,11))
Это очень похоже на определение итератора, приведенное выше, за исключением того, что полный диапазон предопределен от 0 до 10.
Я только что нашел xrange () (удивился, что не видел его раньше ...) и добавил его в приведенный выше пример. xrange () - это итеративная версия классифицировать(), которая имеет то преимущество, что не создает список заранее. Было бы очень полезно, если бы у вас был гигантский корпус данных для итерации, а для этого было бы достаточно памяти.
Вы все равно должны использовать xrange в 2._, потому что 2to3 переводит его автоматически.
Объекты итератора в Python соответствуют протоколу итератора, что в основном означает, что они предоставляют два метода: __iter__() и __next__().
__iter__ возвращает объект-итератор и неявно вызывается
в начале петель.
Метод __next__() возвращает следующее значение и неявно вызывается при каждом приращении цикла. Этот метод вызывает исключение StopIteration, когда больше нет значения для возврата, которое неявно фиксируется конструкциями цикла для прекращения итерации.
Вот простой пример счетчика:
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)
Это напечатает:
3
4
5
6
7
8
Это проще написать с помощью генератора, как описано в предыдущем ответе:
def counter(low, high):
current = low
while current < high:
yield current
current += 1
for c in counter(3, 9):
print(c)
Печатный результат будет таким же. Под капотом объект-генератор поддерживает протокол итератора и делает что-то примерно похожее на класс Counter.
Статья Дэвида Мертца Итераторы и простые генераторы - довольно хорошее введение.
В основном это хороший ответ, но тот факт, что он возвращает self, немного неоптимален. Например, если вы использовали один и тот же объект счетчика в дважды вложенном цикле for, вы, вероятно, не получите того поведения, которое имели в виду.
Нет, итераторы ДОЛЖНЫ возвращать сами себя. Итерируемые объекты возвращают итераторы, но итерируемые объекты не должны реализовывать __next__. counter - это итератор, но не последовательность. Он не хранит своих ценностей. Например, вам не следует использовать счетчик в двояковложенном цикле for.
В примере счетчика self.current должен быть назначен в __iter__ (в дополнение к __init__). В противном случае объект может быть повторен только один раз. Например, если вы скажете ctr = Counters(3, 8), вы не сможете использовать for c in ctr более одного раза.
не должен ли код __iter__ устанавливать значение self.current?
@Curt: Абсолютно нет. Counter - это итератор, и предполагается, что итераторы повторяются только один раз. Если вы сбросите self.current в __iter__, то вложенный цикл над Counter будет полностью нарушен, и все виды предполагаемого поведения итераторов (что вызов iter на них идемпотентен) будут нарушены. Если вы хотите иметь возможность повторять ctr более одного раза, он должен быть итератором без итератора, где он будет возвращать новый итератор при каждом вызове __iter__. Попытка смешивания и сопоставления (итератор, который неявно сбрасывается при вызове __iter__) нарушает протоколы.
Например, если Counter должен был быть итератором без итератора, вы бы полностью удалили определение __next__ / next и, вероятно, переопределили __iter__ как функцию генератора той же формы, что и генератор, описанный в конце этого ответа (кроме вместо границ, исходящих из аргументов __iter__, они будут аргументами __init__, сохраненными на self и доступными из self в __iter__).
Кстати, если вы хотите написать переносимые классы итераторов, полезно определить next или __next__, а затем присвоить одно имя другому (next = __next__ или __next__ = next в зависимости от имени, которое вы дали методу). Определение обоих имен означает, что он работает как с Py2, так и с Py3 без изменений исходного кода.
Спасибо за ответ. Чтобы прояснить двусмысленность: __iter__() вызывает однажды перед входом в конструкцию цикла. «... в начале цикла» предполагает, что __iter__ вызывается в начале каждого цикла в той же конструкции цикла, что неверно. Двойной вложенный цикл for с использованием Counter покажет, что __iter__ вызывается каждый раз один раз перед выполнением вложенного цикла for.
Есть четыре способа построить итеративную функцию:
__iter__ и __next__ (или next в Python 2.x))__getitem__)Примеры:
# 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]
Чтобы увидеть все четыре метода в действии:
for iterator in uc_gen, uc_genexp, uc_iter, uc_getitem:
for ch in iterator('abcde'):
print(ch, end=' ')
print()
Что приводит к:
A B C D E
A B C D E
A B C D E
A B C D E
Примечание:
Два типа генераторов (uc_gen и uc_genexp) не могут быть reversed(); простому итератору (uc_iter) потребуется магический метод __reversed__ (который, согласно документам, должен возвращать новый итератор, но возврат self работает (по крайней мере, в CPython)); и итерация getitem (uc_getitem) должна иметь магический метод __len__:
# 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)
Чтобы ответить на вторичный вопрос полковника Паника о бесконечном лениво вычисляемом итераторе, вот эти примеры, использующие каждый из четырех методов, описанных выше:
# 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
В результате (по крайней мере, для моего пробного запуска):
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
Как выбрать, какой использовать? Это в основном дело вкуса. Чаще всего я вижу два метода: генераторы и протокол итератора, а также гибрид (__iter__, возвращающий генератор).
Выражения генератора полезны для замены понимания списков (они ленивы и поэтому могут экономить ресурсы).
Если вам нужна совместимость с более ранними версиями Python 2.x, используйте __getitem__.
Мне нравится это резюме, потому что оно полное. Эти три способа (yield, выражение генератора и итератор) по сути одинаковы, хотя некоторые из них более удобны, чем другие. Оператор yield фиксирует «продолжение», которое содержит состояние (например, индекс, до которого мы дошли). Информация сохраняется в «закрытии» продолжения. Способ итератора сохраняет ту же информацию внутри полей итератора, что по сути то же самое, что и замыкание. Метод getitem немного отличается, потому что он индексирует содержимое и не является итеративным по своей природе.
Вы не увеличиваете индекс в своем последнем подходе, uc_getitem(). На самом деле, размышляя, он не должен увеличивать индекс, потому что он не поддерживает его. Но это также не способ абстрактной итерации.
@metaperl: Вообще-то, это так. Во всех четырех вышеупомянутых случаях вы можете использовать один и тот же код для итерации.
@EthanFurman Я не специалист, но не должно ли быть сброса индекса в классе uc_iter? Т.е. внутри метода iter установите для self.index значение 0, чтобы сработал следующий вызов итератора
@Asterisk: Нет, экземпляр uc_iter должен истечь, когда это будет сделано (в противном случае это было бы бесконечно); если вы хотите сделать это снова, вам нужно получить новый итератор, снова позвонив uc_iter().
@TerrenceBrannon uc_getitem () работает, но, возможно, ее можно рассматривать как вариант обратной совместимости. Он останавливается, когда возникает IndexError..
Вы можете установить self.index = 0 в __iter__, чтобы можно было повторять много раз. Иначе не получится.
Если бы вы могли сэкономить время, я был бы признателен за объяснение, почему вы предпочли бы любой из этих методов.
@JohnStrood Абсолютно нет, потому что это нарушит протокол итератора. Ожидается, что итераторы просто вернутся в __iter__(), поэтому iter(iterator_instance) не изменяет состояние данного экземпляра итератора.
@JohnStrood для повторения сложного объекта MyClass более одного раза, создайте класс MyClassIterator только с __init__ и __next__ и верните его экземпляр из MyClass.__iter__ (например, return MyClassIterator(self)) вместо self, чтобы MyClassIterator мог хранить ссылку на экземпляр MyClass а также текущий индекс данных, что делает его безопасным для одновременного вызова несколько раз, при этом удовлетворяя другие проблемы протокола итератора.
@aaaaaa: Добавлен раздел "Как выбрать" (внизу).
@VaradhanWork: Спасибо за предложение! В будущем, пожалуйста, используйте комментарии, чтобы указывать на недостатки, вместо того, чтобы вносить большие правки.
@EthanFurman Не могли бы вы объяснить, почему мы делаем это после добавления перевернутый: self.index += -1 if self.index < 0 Когда индекс будет меньше 0?
@coderWorld: __reversed__ устанавливает self.index на -1, поэтому каждый вызов __next__ будет уменьшать его еще на один - это имеет эффект возврата: 'red'[-1] -> 'd', 'red'[-2] -> 'e', 'red'[-3] -> 'r', 'red'[-4] -> StopIteration
Я вижу, что некоторые из вас используют return self в __iter__. Я просто хотел отметить, что сам __iter__ может быть генератором (что устраняет необходимость в __next__ и вызывает исключения StopIteration)
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
Конечно, здесь можно было бы напрямую создать генератор, но для более сложных классов это может быть полезно.
Большой! Так скучно писать просто return self в __iter__. Когда я собирался попробовать использовать в нем yield, я обнаружил, что ваш код делает именно то, что я хочу попробовать.
Но как в этом случае внедрить next()? return iter(self).next()?
@Lenna, он уже «реализован», потому что iter (self) возвращает итератор, а не экземпляр диапазона.
@Manux iter(range(5,10)).next() немного громоздок. По общему признанию, плохой пример поведения next. Меня все еще интересует, как дать экземпляру диапазона атрибут next.
Это самый простой способ сделать это, и он не требует отслеживания, например, self.current или любой другой счетчик. Это должен быть лучший ответ!
Разница: __iter__, являющийся генератором, - это другой объект, чем экземпляр range(). Иногда это имеет значение, иногда нет.
В любом случае вам не следует использовать iter(range(5,10)).next(). Правильный способ - next(iter(range(5,10))). Встроенный next присутствует именно здесь, поэтому вам не нужно беспокоиться о том, будет ли возвращен self в этой ситуации.
проголосовало за - этот метод также больше похож на ожидаемый (относительно принятого ответа) для чего-то вроде r = range(5); list_of_lists = list([ri, list(r)] for ri in r)
Интересно, что __iter__ не должен поднимать StopIteration. Проблема с определением только __iter__ заключается в том, что next(myiterator) не работает, если __next__ не поддерживает отдельные элементы return. Использование next(iter(myiterator)) - не лучшая замена.
Чтобы было ясно, этот подход делает ваш класс повторяемый, но не итератор. Вы получаете новый итераторы каждый раз, когда вызываете iter для экземпляров класса, но сами они не являются экземплярами класса.
@MadPhysicist: на Python 2 iter(range(5,10)).next() и next(iter(range(5,10))) уже полностью эквивалентны. Преимущество next как функции не имеет ничего общего с тем, возвращается ли self__iter__ (поведение одинаково для обоих фрагментов кода). Преимущества встроенной функции next: 1. Она работает одинаково с Py2 и Py3, даже если метод меняет имена между ними и 2. Когда это применимо, ему можно дать второй аргумент для возврата в случае, если итератор уже исчерпан, а не поднять StopIteration.
Это итеративная функция без yield. Он использует функцию iter и закрытие, которое сохраняет его состояние в изменяемом (list) в охватывающей области для 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)
Для Python 3 состояние закрытия сохраняется в неизменяемом объекте области видимости, а nonlocal используется в локальной области видимости для обновления переменной состояния.
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)
Тест;
for i in count(1,10):
print(i)
1
2
3
4
5
6
7
8
9
Я всегда ценю умное использование iter с двумя аргументами, но для ясности: это более сложно и менее эффективно, чем просто использование функции генератора на основе yield; Python имеет множество интерпретаторов, поддерживающих функции генератора на основе yield, которыми вы не можете воспользоваться здесь, что делает этот код значительно медленнее. Тем не менее проголосовали за.
Этот вопрос касается итерационных объектов, а не итераторов. В Python последовательности также являются итерируемыми, поэтому один из способов сделать итеративный класс - это заставить его вести себя как последовательность, то есть дать ему методы __getitem__ и __len__. Я тестировал это на Python 2 и 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__(). Одного __getitem__ с ожидаемым поведением достаточно.
Если вы ищете что-то короткое и простое, возможно, вам будет достаточно:
class A(object):
def __init__(self, l):
self.data = l
def __iter__(self):
return iter(self.data)
пример использования:
In [3]: a = A([2,3,4])
In [4]: [i for i in a]
Out[4]: [2, 3, 4]
Вдохновленный ответом Мэтта Грегори, вот немного более сложный итератор, который вернет 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)
Все ответы на этой странице действительно хороши для сложного объекта. Но для тех, кто содержит встроенные итерируемые типы в качестве атрибутов, например str, list, set или dict, или любую реализацию collections.Iterable, вы можете опустить некоторые вещи в своем классе.
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)
Его можно использовать как:
for x in Test("abcde"):
print(x)
# prints
# a
# b
# c
# d
# e
Как вы сказали, строка уже является итерируемой, поэтому почему дополнительное выражение генератора между ними вместо того, чтобы просто запрашивать строку для итератора (что выражение генератора делает внутренне): return iter(self.string).
@BlackJack Вы действительно правы. Не знаю, что меня убедило так писать. Возможно, я пытался избежать путаницы в ответе, пытаясь объяснить работу синтаксиса итератора с точки зрения синтаксиса итератора.
Включите следующий код в свой код класса.
def __iter__(self):
for x in self.iterable:
yield x
Убедитесь, что вы заменили self.iterable на итерацию, которую вы повторяете.
Вот пример кода
class someClass:
def __init__(self,list):
self.list = list
def __iter__(self):
for x in self.list:
yield x
var = someClass([1,2,3,4,5])
for num in var:
print(num)
Выход
1
2
3
4
5
Примечание: поскольку строки также являются итеративными, их также можно использовать в качестве аргумента для класса.
foo = someClass("Python")
for x in foo:
print(x)
Выход
P
y
t
h
o
n
начиная с python 3.0 больше нет xrange (), а новый range () ведет себя как старый xrange ()