Створіть базовий ітератор Python


Відповіді:


649

Об'єкти ітератора в 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__(крім in __init__). В іншому випадку об’єкт можна повторити лише один раз. Наприклад, якщо ви скажете ctr = Counters(3, 8), ви не можете використовувати for c in ctrбільше одного разу.
Керт

7
@Curt: Абсолютно ні. 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
Мені подобається цей резюме, тому що він повний. Ці три способи (вихід, експресія генератора та ітератор) по суті однакові, хоча деякі зручніші, ніж інші. Оператор урожайності фіксує "продовження", яке містить стан (наприклад, індекс, який ми маємо). Інформація зберігається під час "закриття" продовження. Шлях ітератора зберігає ту саму інформацію всередині полів ітератора, що по суті є тим же, що і закриття. Метод getitem дещо інший, тому що він індексує вміст і не має ітераційного характеру.
Ян

2
@metaperl: Власне, так і є. У всіх чотирьох вищевказаних випадках ви можете використовувати один і той же код для ітерації.
Етан Фурман

1
@Asterisk: Ні, примірник uc_iterповинен закінчитися, коли це буде зроблено (інакше це буде нескінченно); якщо ви хочете зробити це знову, вам потрібно отримати новий ітератор, зателефонувавши uc_iter()ще раз.
Етан Фурман

2
Ви можете встановити self.index = 0в __iter__так що ви можете повторювати багато раз. Інакше ти не можеш.
Джон Строд

1
Якщо ви можете витратити час, я вдячний поясненню того, чому ви б обрали будь-який із методів, ніж інші.
aaaaaa

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 () - це ітерабельна версія діапазону (), яка має перевагу в тому, що не створювати список. Було б дуже корисно, якби у вас був гігантський корпус даних для повторення і у вас було лише стільки пам'яті, щоб це зробити.


20
станом на python 3.0 більше немає xrange (), а новий діапазон () поводиться як старий xrange ()

6
Ви все одно повинні використовувати xrange в 2._, оскільки 2to3 перекладає його автоматично.
Phob

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

Звичайно, тут можна безпосередньо зробити генератор, але для складніших класів це може бути корисно.


5
Чудово! Це так нудно писати просто return selfв __iter__. Коли я збирався спробувати використовувати yieldйого, я виявив, що ваш код робить саме те, що я хочу спробувати.
Рей

3
Але в цьому випадку, як би один реалізувати next()? return iter(self).next()?
Ленна

4
@Lenna, це вже "реалізовано", оскільки iter (self) повертає ітератор, а не екземпляр діапазону.
Манукс

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)

1
Він не повинен мати __len__()метод. __getitem__одне з очікуваною поведінкою достатньо.
BlackJack

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

@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)
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.