Построить базовый итератор Python


427

Как создать итеративную функцию (или объект-итератор) в python?

+3

Здесь два вопроса, оба важных. Как сделать класс итерабельным (т. Е. С чем вы можете перебирать)? И как создать функцию, которая возвращает последовательность с ленивой оценкой? 12 июл. 152015-07-12 13:26:31

+4

Хорошее упражнение, я думаю, это написать класс, представляющий четные числа (бесконечную последовательность). 12 июл. 152015-07-12 13:30:02

+1

@ColonelPanic: Хорошо, добавили пример бесконечного числа в [мой ответ] (http://stackoverflow.com/a/7542261/208880). 04 фев. 162016-02-04 17:25:03

499

Объекты Iterator в python соответствуют протоколу итератора, что в основном означает, что они обеспечивают два метода: __iter__() и next(). __iter__ возвращает объект итератора и неявно вызывается в начале циклов. Метод next() возвращает следующее значение и неявно вызывается при каждом приращении цикла. next() вызывает исключение StopIteration, когда больше нет значения для возврата, которое неявно захватывается с помощью циклов, чтобы остановить итерацию.

Вот простой пример счетчика:

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 

Это будет печатать:

3 
4 
5 
6 
7 
8 

Это проще писать с помощью генератора, так как покрыты в предыдущем ответе:

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

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

Печатный выход будет таким же. Под капотом объект-генератор поддерживает протокол итератора и делает что-то примерно похожее на счетчик классов.

Статья Дэвида Мерца, Iterators and Simple Generators, является довольно хорошим представлением.

+45

Обратите внимание, что функция 'next()' не дает значения 'yield', она' возвращает 'их. 17 окт. 122012-10-17 07:03:44

+46

Это недействительно в Python 3 --- оно должно быть '__next __()'. 23 сен. 132013-09-23 03:42:21

+2

Это в основном хороший ответ, но тот факт, что он возвращает себя, немного не оптимален. Например, если вы использовали один и тот же объект-счетчик в цикле с двойной вставкой, вы, вероятно, не получите того поведения, которое вы имели в виду. 06 фев. 142014-02-06 23:33:31

+12

Нет, итераторам СЛЕДУЕТ вернуться. Итераторы возвращают итераторы, но iterables не должны реализовывать '__next__'. 'counter' - это итератор, но это не последовательность. Он не сохраняет свои значения. Например, вы не должны использовать счетчик в двойном вложенном цикле for-loop. 21 фев. 142014-02-21 08:42:44

+3

В примере счетчика self.current следует назначать в '__iter__' (в дополнение к' __init__'). В противном случае объект может быть повторен только один раз. Например, если вы говорите 'ctr = Counters (3, 8)', то вы не можете использовать 'for c in ctr' более одного раза. 05 апр. 162016-04-05 23:00:00

  0

не должен ли \ _ \ _ iter \ _ \ _ код устанавливать значение self.current? 13 мар. 172017-03-13 02:01:54

  0

@ Курт: Абсолютно нет. «Счетчик» - это итератор, и итераторы должны повторяться только один раз. Если вы сбросите 'self.current' в' __iter__', тогда вложенный цикл над 'Counter' будет полностью нарушен, и всевозможные предполагаемые поведения итераторов (которые называют' iter' на них идемпотентными) нарушаются. Если вы хотите иметь возможность итерации 'ctr' более одного раза, он должен быть итератором без итератора, где он возвращает новый итератор каждый раз, когда вызывается' __iter__'. Попытка смешивания и сопоставления (итератор, который неявно сбрасывается при вызове '__iter__'), нарушает протоколы. 24 фев. 182018-02-24 01:16:43

  0

Например, если 'Counter' должен быть итератором без итератора, вы полностью удалите определение' __next__'/'next' и, возможно, переопределите' __iter__' как функцию генератора той же формы, что и генератор описанные в конце этого ответа (за исключением границ, исходящих из аргументов в '__iter__', они будут аргументами' __init__', сохраненными на 'self' и доступными из' self' в '__iter__'). 24 фев. 182018-02-24 01:19:21

  0

BTW, полезная вещь, которую нужно сделать, если вы хотите написать переносимые классы итераторов, - это определить «следующий» или «__next__», а затем присвоить одно имя другому («next = __next__» или «__next__ = next» в зависимости от имя, которое вы дали методу). Определение обоих имен означает, что он работает как с Py2, так и с Py3 без изменений исходного кода. 24 фев. 182018-02-24 01:43:52


97

Прежде всего itertools module невероятно полезно для всех видов случаев, в которых итератор был бы полезен, но здесь все, что нужно для создания итератора в Python:

выход

Разве это не круто? Выход можно использовать для замены нормального возврата в функции. Он возвращает объект одинаково, но вместо уничтожения состояния и выхода он сохраняет состояние, когда вы хотите выполнить следующую итерацию. Вот пример его в действие берутся непосредственно из itertools function list:

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

Как указано в описании функций (это граф() функции из модуля itertools ...), она производит итератор, возвращает последовательные целые числа, начиная с n.

Generator expressions это целая другая червь червей (удивительные черви!). Они могут использоваться вместо List Comprehension для сохранения памяти (для понимания списка создается список в памяти, который уничтожается после использования, если не назначен переменной, но выражения генератора могут создавать объект генератора ... что является причудливым способом сказать Итератор). Ниже приведен пример определения экспрессии генератора:

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

Это очень похоже нашему определению выше, за исключением итератора полного диапазона предопределено быть между 0 и 10.

Я только что нашел xrange() (это я не видел раньше ...) и добавил его к приведенному выше примеру. xrange() - это итерируемая версия range(), которая имеет то преимущество, что не является предварительным составлением списка. Было бы очень полезно, если бы у вас был гигантский массив данных для перебора и у него было столько памяти, чтобы сделать это.

+17

Начиная с python 3.0 больше нет xrange(), а новый диапазон() ведет себя как старый xrange() 18 дек. 082008-12-18 17:30:08

+6

Вы все равно должны использовать xrange в 2._, потому что 2to3 автоматически переводит его. 22 июл. 112011-07-22 18:03:40


305

Есть четыре способа построить итерационную функцию:

  • создать генератор (использует yield keyword)
  • использовать выражение генератора (genexp)
  • создать итератор (определяет __iter__ and __next__ (или next в Python 2.x))
  • создать функцию, которую Python может выполнять самостоятельно (defines __getitem__)

Примеры:

# 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 

Чтобы увидеть все четыре метода в действии:

for iterator in uc_gen, uc_genexp, uc_iter, uc_getitem: 
    for ch in iterator('abcde'): 
     print ch, 
    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__ (который должен возвращать новый итератор, который идет назад); и GetItem iteratable (uc_getitem) должен иметь метод __len__ магии:

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

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

Для того, чтобы ответить на вторичный вопрос полковника Panic в о бесконечном лениво оцениваемом итераторе, вот эти примеры, используя каждый из четырех методов выше:

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

Мне нравится это резюме, потому что он завершен. Эти три способа (выход, выражение генератора и итератор) по существу одинаковы, хотя некоторые из них более удобны, чем другие. Оператор yield фиксирует «продолжение», которое содержит состояние (например, индекс, который мы делаем). Информация сохраняется в «закрытии» продолжения. Итератор способ сохраняет одну и ту же информацию внутри полей итератора, что по сути является тем же, что и закрытие. Метод __getitem__ немного отличается, потому что он индексируется в содержимое и не является итеративным по своей природе. 05 июл. 132013-07-05 01:04:22

  0

Вы не увеличиваете индекс в своем последнем подходе, 'uc_getitem()'.Фактически при отражении он не должен увеличивать индекс, потому что он не поддерживает его. Но это также не способ абстрагировать итерацию. 05 ноя. 132013-11-05 15:25:55

+2

@metaperl: Собственно, это так. Во всех четырех случаях вы можете использовать один и тот же код для итерации. 05 ноя. 132013-11-05 16:37:21


79

Я вижу, что некоторые из вас делают 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 

Конечно здесь можно было бы так же непосредственно сделать генератор, но для более сложных классов это может быть полезным.

+5

Отлично! Это так скучно писать просто 'возвращение себя' в' __iter__'. Когда я собирался попробовать использовать 'yield', я обнаружил, что ваш код выполняет именно то, что я хочу попробовать. 05 фев. 132013-02-05 19:32:35

+2

Но в этом случае, как бы реализовать «next()»? 'return iter (self) .next()'? 05 апр. 132013-04-05 19:52:12

+4

@Lenna, он уже «реализован», потому что iter (self) возвращает итератор, а не экземпляр диапазона. 07 апр. 132013-04-07 17:31:31

  0

@Manux 'iter (диапазон (5,10)). Next()' немного громоздко. По общему признанию, это плохой пример поведения 'next'. Меня все еще интересует, как указать экземпляр диапазона как атрибут 'next'. 24 апр. 132013-04-24 19:06:36

+2

Это самый простой способ сделать это и не требует отслеживания, например. '' self.current'' или любой другой счетчик. Это должен быть голосовой ответ! 31 мар. 142014-03-31 13:35:53

  0

Разница: '__iter__' является генератором другого объекта, чем экземпляр' range() '. Иногда это имеет значение, иногда это не так. 09 ноя. 142014-11-09 19:42:19

  0

Вы не должны использовать 'iter (range (5,10)). Next()' anyway. Правильный путь - 'next (iter (range (5,10))). «Следующая» встроена именно так, что вам не нужно заботиться о том, возвращается ли «сама» в этой ситуации. 14 мар. 162016-03-14 20:41:59

  0

up-voted - этот метод также больше похож на ожидаемый (относительно принятого ответа) на что-то вроде 'r = range (5); list_of_lists = list ([ri, list (r)] для ri in r) ' 17 сен. 162016-09-17 16:09:23

  0

Интересно, что' __iter__' не нужно поднимать StopIteration. Проблема с определением только '__iter__' заключается в том, что' next (myiterator) 'не работает, если' __next__' не 'возвращает' отдельные элементы. Необходимость использования 'next (iter (myiterator))' не является мудрой заменой. 18 апр. 172017-04-18 14:42:52

  0

Чтобы быть понятным, этот подход делает ваш класс * итерируемым *, но не * итератором *. Вы получаете свежие * итераторы * каждый раз, когда вы вызываете 'iter' в экземплярах класса, но они не являются самими экземплярами класса. 24 фев. 182018-02-24 01:25:07

  0

@MadPhysicist: на Python 2, 'iter (диапазон (5,10)). Next()' и 'next (iter (range (5,10)))' уже точно эквивалентны. Преимущество 'next' как функции не имеет никакого отношения к тому, возвращается ли' self' '__iter__' (поведение идентично для обоих фрагментов кода). Преимущества встроенной функции 'next': 1. Она работает одинаково на Py2 и Py3, хотя метод изменяет имена между ними и 2. Когда это применимо, ему может быть предоставлен второй аргумент для возврата в событие что итератор уже исчерпан, а не поднимает 'StopIteration'. 24 фев. 182018-02-24 01:27:54


3

Это итеративная функция без 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 
  0

Я всегда ценю умное использование двух-arg 'iter', но просто для того, чтобы быть ясным: это более сложный и менее эффективный, чем просто использование функции генератора на основе' yield'; Python имеет тонну поддержки интерпретатора для функций генератора на основе «yield», которые вы не можете использовать здесь, делая этот код значительно медленнее. Тем не менее, проголосовали. 24 фев. 182018-02-24 01:30:05


7

Этот вопрос касается истребимых объектов, а не об итераторах. В 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)