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

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

Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
594
0
376 531
10
Перейти к ответу Данный вопрос помечен как решенный

Ответы 10

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

начиная с python 3.0 больше нет xrange (), а новый range () ведет себя как старый xrange ()

user3850 18.12.2008 20:30

Вы все равно должны использовать xrange в 2._, потому что 2to3 переводит его автоматически.

Phob 22.07.2011 22:03
Ответ принят как подходящий

Объекты итератора в 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, вы, вероятно, не получите того поведения, которое имели в виду.

Casey Rodarmor 07.02.2014 03:33

Нет, итераторы ДОЛЖНЫ возвращать сами себя. Итерируемые объекты возвращают итераторы, но итерируемые объекты не должны реализовывать __next__. counter - это итератор, но не последовательность. Он не хранит своих ценностей. Например, вам не следует использовать счетчик в двояковложенном цикле for.

leewz 21.02.2014 12:42

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

Curt 06.04.2016 02:00

не должен ли код __iter__ устанавливать значение self.current?

kdubs 13.03.2017 05:01

@Curt: Абсолютно нет. Counter - это итератор, и предполагается, что итераторы повторяются только один раз. Если вы сбросите self.current в __iter__, то вложенный цикл над Counter будет полностью нарушен, и все виды предполагаемого поведения итераторов (что вызов iter на них идемпотентен) будут нарушены. Если вы хотите иметь возможность повторять ctr более одного раза, он должен быть итератором без итератора, где он будет возвращать новый итератор при каждом вызове __iter__. Попытка смешивания и сопоставления (итератор, который неявно сбрасывается при вызове __iter__) нарушает протоколы.

ShadowRanger 24.02.2018 04:16

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

ShadowRanger 24.02.2018 04:19

Кстати, если вы хотите написать переносимые классы итераторов, полезно определить next или __next__, а затем присвоить одно имя другому (next = __next__ или __next__ = next в зависимости от имени, которое вы дали методу). Определение обоих имен означает, что он работает как с Py2, так и с Py3 без изменений исходного кода.

ShadowRanger 24.02.2018 04:43

Спасибо за ответ. Чтобы прояснить двусмысленность: __iter__() вызывает однажды перед входом в конструкцию цикла. «... в начале цикла» предполагает, что __iter__ вызывается в начале каждого цикла в той же конструкции цикла, что неверно. Двойной вложенный цикл for с использованием Counter покажет, что __iter__ вызывается каждый раз один раз перед выполнением вложенного цикла for.

Minh Tran 20.05.2018 00:24

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

  • создать генератор (использует ключевое слово yield)
  • используйте выражение генератора (genexp)
  • создать итератор (определяет __iter__ и __next__ (или next в Python 2.x))
  • создать класс, который Python может перебирать самостоятельно (определяет __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 немного отличается, потому что он индексирует содержимое и не является итеративным по своей природе.

Ian 05.07.2013 05:04

Вы не увеличиваете индекс в своем последнем подходе, uc_getitem(). На самом деле, размышляя, он не должен увеличивать индекс, потому что он не поддерживает его. Но это также не способ абстрактной итерации.

Terrence Brannon 05.11.2013 19:25

@metaperl: Вообще-то, это так. Во всех четырех вышеупомянутых случаях вы можете использовать один и тот же код для итерации.

Ethan Furman 05.11.2013 20:37

@EthanFurman Я не специалист, но не должно ли быть сброса индекса в классе uc_iter? Т.е. внутри метода iter установите для self.index значение 0, чтобы сработал следующий вызов итератора

Asterisk 19.04.2018 12:30

@Asterisk: Нет, экземпляр uc_iter должен истечь, когда это будет сделано (в противном случае это было бы бесконечно); если вы хотите сделать это снова, вам нужно получить новый итератор, снова позвонив uc_iter().

Ethan Furman 19.04.2018 19:13

@TerrenceBrannon uc_getitem () работает, но, возможно, ее можно рассматривать как вариант обратной совместимости. Он останавливается, когда возникает IndexError..

Josiah Yoder 27.07.2018 17:35

Вы можете установить self.index = 0 в __iter__, чтобы можно было повторять много раз. Иначе не получится.

John Strood 14.08.2018 11:26

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

aaaaaa 21.01.2019 07:15

@JohnStrood Абсолютно нет, потому что это нарушит протокол итератора. Ожидается, что итераторы просто вернутся в __iter__(), поэтому iter(iterator_instance) не изменяет состояние данного экземпляра итератора.

BlackJack 27.06.2019 17:02

@JohnStrood для повторения сложного объекта MyClass более одного раза, создайте класс MyClassIterator только с __init__ и __next__ и верните его экземпляр из MyClass.__iter__ (например, return MyClassIterator(self)) вместо self, чтобы MyClassIterator мог хранить ссылку на экземпляр MyClass а также текущий индекс данных, что делает его безопасным для одновременного вызова несколько раз, при этом удовлетворяя другие проблемы протокола итератора.

simpleuser 27.08.2019 23:40

@aaaaaa: Добавлен раздел "Как выбрать" (внизу).

Ethan Furman 28.08.2019 18:57

@VaradhanWork: Спасибо за предложение! В будущем, пожалуйста, используйте комментарии, чтобы указывать на недостатки, вместо того, чтобы вносить большие правки.

Ethan Furman 12.02.2020 20:30

@EthanFurman Не могли бы вы объяснить, почему мы делаем это после добавления перевернутый: self.index += -1 if self.index < 0 Когда индекс будет меньше 0?

coderWorld 13.04.2020 02:33

@coderWorld: __reversed__ устанавливает self.index на -1, поэтому каждый вызов __next__ будет уменьшать его еще на один - это имеет эффект возврата: 'red'[-1] -> 'd', 'red'[-2] -> 'e', 'red'[-3] -> 'r', 'red'[-4] -> StopIteration

Ethan Furman 17.04.2020 05:15

Я вижу, что некоторые из вас используют 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, я обнаружил, что ваш код делает именно то, что я хочу попробовать.

Ray 05.02.2013 23:32

Но как в этом случае внедрить next()? return iter(self).next()?

Lenna 05.04.2013 23:52

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

Manux 07.04.2013 21:31

@Manux iter(range(5,10)).next() немного громоздок. По общему признанию, плохой пример поведения next. Меня все еще интересует, как дать экземпляру диапазона атрибут next.

Lenna 24.04.2013 23:06

Это самый простой способ сделать это, и он не требует отслеживания, например, self.current или любой другой счетчик. Это должен быть лучший ответ!

astrofrog 31.03.2014 17:35

Разница: __iter__, являющийся генератором, - это другой объект, чем экземпляр range(). Иногда это имеет значение, иногда нет.

Ethan Furman 09.11.2014 22:42

В любом случае вам не следует использовать iter(range(5,10)).next(). Правильный способ - next(iter(range(5,10))). Встроенный next присутствует именно здесь, поэтому вам не нужно беспокоиться о том, будет ли возвращен self в этой ситуации.

Mad Physicist 14.03.2016 23:41

проголосовало за - этот метод также больше похож на ожидаемый (относительно принятого ответа) для чего-то вроде r = range(5); list_of_lists = list([ri, list(r)] for ri in r)

swinman 17.09.2016 19:09

Интересно, что __iter__ не должен поднимать StopIteration. Проблема с определением только __iter__ заключается в том, что next(myiterator) не работает, если __next__ не поддерживает отдельные элементы return. Использование next(iter(myiterator)) - не лучшая замена.

Acumenus 18.04.2017 17:42

Чтобы было ясно, этот подход делает ваш класс повторяемый, но не итератор. Вы получаете новый итераторы каждый раз, когда вызываете iter для экземпляров класса, но сами они не являются экземплярами класса.

ShadowRanger 24.02.2018 04:25

@MadPhysicist: на Python 2 iter(range(5,10)).next() и next(iter(range(5,10))) уже полностью эквивалентны. Преимущество next как функции не имеет ничего общего с тем, возвращается ли self__iter__ (поведение одинаково для обоих фрагментов кода). Преимущества встроенной функции next: 1. Она работает одинаково с Py2 и Py3, даже если метод меняет имена между ними и 2. Когда это применимо, ему можно дать второй аргумент для возврата в случае, если итератор уже исчерпан, а не поднять StopIteration.

ShadowRanger 24.02.2018 04:27

Это итеративная функция без 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, которыми вы не можете воспользоваться здесь, что делает этот код значительно медленнее. Тем не менее проголосовали за.

ShadowRanger 24.02.2018 04:30

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

BlackJack 27.06.2019 17:05

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

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 27.06.2019 17:07

@BlackJack Вы действительно правы. Не знаю, что меня убедило так писать. Возможно, я пытался избежать путаницы в ответе, пытаясь объяснить работу синтаксиса итератора с точки зрения синтаксиса итератора.

John Strood 27.06.2019 19:40

Включите следующий код в свой код класса.

 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

Другие вопросы по теме