Создайте базовый итератор Python

Ответы:

650

Итераторские объекты в 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.

Статья Дэвида Мерца « Итераторы и простые генераторы» - довольно хорошее введение.

АРС
источник
4
Это в основном хороший ответ, но тот факт, что он возвращает себя, немного неоптимален. Например, если бы вы использовали один и тот же объект счетчика в цикле с двойным вложением, вы, вероятно, не получили бы то поведение, которое имели в виду.
Кейси Родармор
22
Нет, итераторы ДОЛЖНЫ возвращаться. Итераторы возвращают итераторы, но итерации не должны реализовываться __next__. counterявляется итератором, но это не последовательность. Он не хранит свои значения. Например, не следует использовать счетчик в цикле for с двойным вложением.
Leewz
4
В примере Counter, self.current должен быть назначен в __iter__(в дополнение к в __init__). В противном случае объект может быть повторен только один раз. Например, если вы говорите ctr = Counters(3, 8), то вы не можете использовать for c in ctrболее одного раза.
Курт
7
@ Курт: Абсолютно нет. Counterявляется итератором, и итераторы должны повторяться только один раз. При сбросе self.currentв __iter__, то вложенный цикл над Counterбудет полностью разрушен, и все виды предполагаемого поведения итераторов (что вызов iterна них идемпотентно) нарушается. Если вы хотите иметь возможность повторять итерацию ctrболее одного раза, она должна быть повторяемой без итератора, при которой каждый раз __iter__вызывается новый итератор . Попытка смешать и сопоставить (итератор, который неявно сбрасывается при __iter__вызове) нарушает протоколы.
ShadowRanger
2
Например, если Counterбы итератор был не итератором, вы удалили бы определение целиком __next__/ nextи, возможно, переопределили __iter__бы функцию-генератор той же формы, что и генератор, описанный в конце этого ответа (за исключением случаев, когда вместо границ) исходя из аргументов __iter__, они были бы аргументы , чтобы __init__сохранить на selfи доступны из selfв __iter__).
ShadowRanger
427

Существует четыре способа создания итеративной функции:

Примеры:

# 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 iteratable ( 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__.

Этан Фурман
источник
4
Мне нравится это резюме, потому что оно завершено. Эти три способа (yield, выражение генератора и итератор) по сути одинаковы, хотя некоторые из них более удобны, чем другие. Оператор yield фиксирует «продолжение», в котором содержится состояние (например, индекс, к которому мы пришли). Информация сохраняется в «закрытии» продолжения. Способ итератора сохраняет ту же информацию в полях итератора, что по сути то же самое, что и замыкание. Метод getitem немного отличается, потому что он индексирует содержимое и не является итеративным по своей природе.
Ян
2
@ Metaperl: На самом деле это так. Во всех четырех приведенных выше случаях вы можете использовать один и тот же код для итерации.
Итан Фурман
1
@Asterisk: Нет, экземпляр uc_iterдолжен истечь, когда это будет сделано (в противном случае он будет бесконечным); если вы хотите сделать это снова, вы должны получить новый итератор, позвонив uc_iter()снова.
Итан Фурман
2
Вы можете установить self.index = 0в __iter__так что вы можете повторять много раз. В противном случае вы не можете.
Джон Струд
1
Если бы вы могли сэкономить время, я был бы признателен за объяснение того, почему вы бы выбрали любой из методов по сравнению с другими.
аааааа
103

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

Уступать

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

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

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

Выражения генератора - это еще одна банка червей (удивительные черви!). Они могут быть использованы вместо списка Понимания , чтобы сохранить память (списочные создать список в памяти, уничтожаются после использования , если не назначена переменный, но выражения генератора могут создать генератор объект ... который является причудливым способом говорю итератор). Вот пример определения выражения генератора:

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

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

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

akdom
источник
20
Начиная с Python 3.0 более не существует xrange (), а новый range () ведет себя как старый xrange ()
6
Вы все равно должны использовать xrange в 2._, потому что 2to3 переводит его автоматически.
Фоб
100

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

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

Manux
источник
5
Большой! Это так скучно писать просто return selfв __iter__. Когда я собирался попробовать использовать yieldего, я обнаружил, что ваш код делает именно то, что я хочу попробовать.
Рэй
3
Но как в этом случае реализовать next()? return iter(self).next()?
Ленна
4
@Lenna, она уже «реализована», потому что iter (self) возвращает итератор, а не экземпляр диапазона.
Manux
3
Это самый простой способ сделать это, и он не требует отслеживания, например, self.currentили любого другого счетчика. Это должен быть самый популярный ответ!
астрофрог
4
Чтобы быть понятным, этот подход делает ваш класс повторяемым , но не итератором . Вы получаете новые итераторы каждый раз, когда вызываете iterэкземпляры класса, но сами они не являются экземплярами класса.
ShadowRanger
13

Этот вопрос касается итеративных объектов, а не итераторов. В 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)
AQ2
источник
1
Это не должно иметь __len__()метод. __getitem__одного с ожидаемым поведением достаточно.
Блэкджек,
5

Все ответы на этой странице действительно хороши для сложного объекта. Но для тех , которые содержат встроенный Iterable типов как атрибуты, как 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
Джон Строуд
источник
1
Как вы сказали, строка уже итерация так почему дополнительное выражение генератора между вместо того , чтобы просто попросить строку для итератора (который выражение делает генератор внутренне) return iter(self.string).
Блэкджек
@ BlackJack Ты действительно прав. Я не знаю, что убедило меня написать так. Возможно, я пытался избежать путаницы в ответе, пытаясь объяснить работу синтаксиса итератора в терминах более синтаксиса итератора.
Джон Струд
3

Это итеративная функция без yield. Он использует iterфункцию и замыкание, которое сохраняет свое состояние в mutable ( 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основанных функций генератора, которые вы не можете здесь использовать, что делает этот код значительно медленнее. Тем не менее, за него проголосовали.
ShadowRanger
2

Если вы ищете что-то короткое и простое, возможно, вам этого будет достаточно:

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]
Даниил Машкин
источник
-1

Вдохновленный ответом Мэтта Грегори, здесь есть более сложный итератор, который будет возвращать 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)
Ace.Di
источник