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

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

21.08.2008 00:36:11
9 ОТВЕТОВ
РЕШЕНИЕ

Итераторские объекты в python соответствуют протоколу итератора, что в основном означает, что они предоставляют два метода: __iter__() и __next__().

  • __iter__Возвращает объект итератора и неявно вызывается в начале петли.

  • __next__()Метод возвращает следующее значение и неявно вызывается при каждом приращении цикла. Этот метод вызывает исключение StopItered, когда больше нет возвращаемого значения, которое неявно захватывается циклами для остановки итерации.

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

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.

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

643
30.10.2019 09:16:29
Это в основном хороший ответ, но тот факт, что он возвращает себя, немного неоптимален. Например, если бы вы использовали один и тот же объект счетчика в цикле с двойным вложением, вы, вероятно, не получили бы то поведение, которое имели в виду.
Casey Rodarmor 6.02.2014 23:33:31
Нет, итераторы ДОЛЖНЫ возвращаться. Итераторы возвращают итераторы, но итерации не должны реализовываться __next__. counterявляется итератором, но это не последовательность. Он не хранит свои значения. Например, не следует использовать счетчик в цикле for с двойным вложением.
leewz 21.02.2014 08:42:44
В примере Counter, self.current должен быть назначен в __iter__(в дополнение к в __init__). В противном случае объект может быть повторен только один раз. Например, если вы говорите ctr = Counters(3, 8), то вы не можете использовать for c in ctrболее одного раза.
Curt 5.04.2016 23:00:00
@ Курт: Абсолютно нет. Counterявляется итератором, и итераторы должны повторяться только один раз. При сбросе self.currentв __iter__, то вложенный цикл над Counterбудет полностью разрушен, и все виды предполагаемого поведения итераторов (что вызов iterна них идемпотентно) нарушается. Если вы хотите иметь возможность повторять итерацию ctrболее одного раза, она должна быть повторяемой без итератора, при которой каждый раз __iter__вызывается новый итератор . Попытка смешать и сопоставить (итератор, который неявно сбрасывается при __iter__вызове) нарушает протоколы.
ShadowRanger 24.02.2018 01:16:43
Например, если Counterбы итератор был не итератором, вы бы полностью удалили определение __next__/ nextи, вероятно, переопределили __iter__бы функцию-генератор той же формы, что и генератор, описанный в конце этого ответа (за исключением случаев, когда вместо границ) исходя из аргументов __iter__, они были бы аргументы , чтобы __init__сохранить на selfи доступны из selfв __iter__).
ShadowRanger 24.02.2018 01:19:21

Прежде всего, модуль 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 (), которая имеет преимущество в том, что не создает список заранее. Было бы очень полезно, если бы у вас был огромный массив данных для перебора, и у вас было только столько памяти, чтобы сделать это.

103
27.01.2019 03:25:27
Начиная с Python 3.0 более не существует xrange (), а новый range () ведет себя как старый xrange ()
user3850 18.12.2008 17:30:08
Вы все равно должны использовать xrange в 2._, потому что 2to3 переводит его автоматически.
Phob 22.07.2011 18:03:40

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

Примеры:

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

415
12.02.2020 17:29:30
Мне нравится это резюме, потому что оно завершено. Эти три способа (yield, выражение генератора и итератор) по сути одинаковы, хотя некоторые из них более удобны, чем другие. Оператор yield фиксирует «продолжение», в котором содержится состояние (например, индекс, к которому мы пришли). Информация сохраняется в «закрытии» продолжения. Способ итератора сохраняет ту же информацию в полях итератора, что по сути то же самое, что и замыкание. Метод getitem немного отличается, потому что он индексирует содержимое и не является итеративным по своей природе.
Ian 5.07.2013 01:04:22
@ Metaperl: На самом деле это так. Во всех четырех приведенных выше случаях вы можете использовать один и тот же код для итерации.
Ethan Furman 5.11.2013 16:37:21
@Asterisk: Нет, экземпляр uc_iterдолжен истечь, когда это будет сделано (в противном случае он будет бесконечным); если вы хотите сделать это снова, вы должны получить новый итератор, позвонив uc_iter()снова.
Ethan Furman 19.04.2018 16:13:36
Вы можете установить self.index = 0в __iter__так что вы можете повторять много раз. В противном случае вы не можете.
John Strood 14.08.2018 08:26:20
Если бы вы могли сэкономить время, я был бы признателен за объяснение того, почему вы бы выбрали любой из методов по сравнению с другими.
aaaaaa 21.01.2019 04:15:06

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

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

100
27.07.2012 15:05:12
Большой! Это так скучно писать просто return selfв __iter__. Когда я собирался попробовать использовать yieldего, я обнаружил, что ваш код делает именно то, что я хочу попробовать.
Ray 5.02.2013 19:32:35
Но как в этом случае реализовать next()? return iter(self).next()?
Lenna 5.04.2013 19:52:12
@Lenna, она уже «реализована», потому что iter (self) возвращает итератор, а не экземпляр диапазона.
Manux 7.04.2013 17:31:31
Это самый простой способ сделать это, и он не требует отслеживания, например, self.currentили любого другого счетчика. Это должен быть самый популярный ответ!
astrofrog 31.03.2014 13:35:53
Чтобы было ясно, этот подход делает ваш класс итеративным , но не итератором . Вы получаете новые итераторы каждый раз, когда вызываете iterэкземпляры класса, но сами они не являются экземплярами класса.
ShadowRanger 24.02.2018 01:25:07

Это итеративная функция без 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
3
3.03.2016 17:55:26
Я всегда ценю умное использование двух аргументов iter, но просто для ясности: это более сложно и менее эффективно, чем просто использование yieldоснованной функции генератора; У Python есть масса поддержки интерпретатора для yieldоснованных функций генератора, которые вы не можете здесь использовать, что делает этот код значительно медленнее. Тем не менее, за него проголосовали.
ShadowRanger 24.02.2018 01:30:05

Этот вопрос касается итеративных объектов, а не итераторов. В 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)
12
21.03.2016 17:39:14
Это не должно иметь __len__()метод. __getitem__одного с ожидаемым поведением достаточно.
BlackJack 27.06.2019 14:05:37

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

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]
2
26.04.2018 08:38:39

Вдохновленный ответом Мэтта Грегори здесь немного более сложный итератор, который будет возвращать 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)
-1
13.07.2018 17:34:13

Все ответы на этой странице действительно хороши для сложного объекта. Но для тех , которые содержат встроенный 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
5
27.06.2019 16:45:43
Как вы сказали, строка уже итерация так почему дополнительное выражение генератора между вместо того , чтобы просто попросить строку для итератора (который выражение делает генератор внутренне) return iter(self.string).
BlackJack 27.06.2019 14:07:39
@ BlackJack Ты действительно прав. Я не знаю, что убедило меня написать так. Возможно, я пытался избежать путаницы в ответе, пытаясь объяснить работу синтаксиса итератора в терминах более синтаксиса итератора.
John Strood 27.06.2019 16:40:54