基本的なPythonイテレータを構築する


回答:


650

Pythonのイテレータオブジェクトはイテレータプロトコルに準拠しています。つまり、基本的には、との2つのメソッドを提供__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)

印刷出力は同じになります。内部的には、ジェネレーターオブジェクトはイテレータープロトコルをサポートし、クラスカウンターとほぼ同じような処理を行います。

David Mertzの記事「イテレーターとシンプルなジェネレーター」は、非常に優れた紹介です。


4
これはほとんど良い答えですが、それが自己を返すという事実は少し最適ではありません。たとえば、二重にネストされたforループで同じカウンターオブジェクトを使用した場合、おそらく意図した動作が得られません。
Casey Rodarmor、2014

22
いいえ、イテレータは自分自身を返す必要があります。イテラブルはイテレータを返しますが、イテラブルは実装すべきではありません__next__counterイテレータですが、シーケンスではありません。値は保存されません。たとえば、二重にネストされたforループでカウンターを使用しないでください。
leewz 14

4
Counterの例では、__iter__(に加えて__init__)self.currentを割り当てます。それ以外の場合、オブジェクトは1回だけ反復できます。たとえば、と言った場合、一度ctr = Counters(3, 8)しか使用できませんfor c in ctr
Curt

7
@Curt:絶対にありません。Counterはイテレータであり、イテレータは一度だけ反復されることになっています。でリセットself.currentする__iter__と、のネストされたループCounterが完全に壊れ、イテレータの想定されたすべての動作(呼び出しはべきiter等)に違反します。ctr複数回反復できるようにしたい場合は、反復可能でない反復可能である必要があり、__iter__呼び出されるたびに新しい反復を返します。混合して照合しようとすると(__iter__呼び出されたときに暗黙的にリセットされるイテレータ)、プロトコルに違反します。
ShadowRanger 2018

2
たとえば、Counterイテレータ以外の反復可能である場合は、__next__/ の定義をnext完全に削除し、おそらく__iter__この回答の最後に説明されているジェネレータと同じ形式のジェネレータ関数として再定義します(範囲の代わりに)引数からに来る__iter__彼らは、引数にはするだろう、__init__上に保存selfしてからアクセスself__iter__)。
ShadowRanger 2018

427

反復関数を作成するには4つの方法があります。

例:

# 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]

4つのメソッドすべての動作を確認するには:

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

2つのジェネレーターのタイプ(uc_genおよびuc_genexp)はできませんreversed()。単純なイテレータ(uc_iter)には__reversed__魔法のメソッドが必要になります(docsよれば、新しいイテレータを返す必要がありますが、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)

無限に遅延評価された反復子に関するパニック大佐の二次的な質問に答えるために、上記の4つの方法のそれぞれを使用した例を次に示します。

# 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

どれを使用するかを選択するには?これは主に好みの問題です。私が最もよく目にする2つの方法は、ジェネレーターとイテレータープロトコル、およびハイブリッド(__iter__ジェネレーターを返す)です。

ジェネレータ式は、リスト内包表記を置き換えるのに役立ちます(それらは遅延するため、リソースを節約できます)。

以前のPython 2.xバージョンとの互換性が必要な場合は、を使用してください__getitem__


4
この要約は完全なので、気に入っています。これらの3つの方法(yield、generator expression、およびiterator)は基本的に同じですが、他の方法より便利なものもあります。収量演算子は、状態(たとえば、現在のインデックス)を含む「継続」をキャプチャします。情報は続きの「クロージャー」に保存されます。反復子の方法では、反復子のフィールド内に同じ情報を保存します。これは、本質的にクロージャーと同じです。GetItemメソッドメソッドは、内容にそれインデックスので少し異なっており、本質的に反復的ではありません。
Ian

2
@metaperl:実はそうです。上記の4つのケースすべてで、同じコードを使用して反復できます。
イーサンファーマン2013年

1
@アスタリスク:いいえ、インスタンスがuc_iter終了するとインスタンスの有効期限が切れます(それ以外の場合は無期限になります)。もう一度実行したい場合は、再度呼び出して新しいイテレータを取得する必要がありますuc_iter()
イーサンファーマン2018

2
あなたは、設定することができますself.index = 0__iter__あなたは何度も繰り返すことができるように。そうでなければできません。
John Strood

1
時間を割いていただければ、他の方法よりもいずれかの方法を選択する理由について説明をいただければ幸いです。
aaaaaa

103

まず、itertoolsモジュールは、イテレーターが役立つあらゆる種類のケースで非常に便利ですが、Pythonでイテレーターを作成するために必要なものはすべて次のとおりです。

産出

かっこよくありませんか?Yieldは、関数の通常の戻り値を置き換えるために使用できます。オブジェクトはまったく同じように返されますが、状態を破棄して終了するのではなく、次の反復を実行するときのために状態を保存します。以下はitertools関数リストから直接引かれた動作の例です:

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

関数の説明(itertoolsモジュールのcount()関数です)で述べたように、nで始まる連続した整数を返すイテレータを生成します。

ジェネレーター式は、他の完全なワーム(素晴らしいワーム!)です。彼らはの代わりに使用することができるリスト読解リストの内包表記は、変数に割り当てられていない場合は、使用後に破壊されたメモリ内のリストを作成しますが、ジェネレータ式はジェネレータオブジェクトを作成することができます(メモリを節約するために...の空想の方法ですイテレータと言います)。ジェネレータ式の定義の例を次に示します。

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

これは、全範囲が0〜10の間で事前に決定されていることを除いて、上記のイテレータ定義と非常に似ています。

私はxrange()を見つけて私はこれまでに見たことがないと思います...)、それを上記の例に追加しました。 xrange()range()の反復可能なバージョンであり、リストを事前に作成しないという利点があります。反復する巨大なデータのコーパスがあり、それを実行するのに大量のメモリしかない場合は、非常に便利です。


20
python 3.0以降、xrange()はなくなり、新しいrange()は古いxrange()のように動作します

6
2.toでは2to3が自動的に変換するため、x_rangeを2._で使用する必要があります。
フォブ

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それを使ってみるつもりだったとき、私はあなたのコードが私がやりたいことを正確に実行しているのを見つけました。
Ray

3
しかし、この場合、どのように実装しnext()ますか?return iter(self).next()
レナ2013

4
@ Lenna、iter(self)は範囲インスタンスではなくイテレータを返すため、すでに「実装」されています。
Manux 2013

3
これはそれを行う最も簡単な方法であり、eg self.currentや他のカウンターを追跡する必要はありません。これがトップ投票の答えになるはずです!
astrofrog 2014年

4
明確にするために、このアプローチはクラスを反復可能にしますが、反復子は作成しません。クラスのインスタンスを呼び出すたびに新しいイテレータを取得iterしますが、それ自体はクラスのインスタンスではありません。
ShadowRanger 2018

13

この質問は、反復子についてではなく、反復可能なオブジェクトについてです。Pythonでは、シーケンスも反復可能であるため、反復可能クラスを作成する1つの方法は、シーケンスのように動作させること、つまり、シーケンス__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

このページのすべての回答は、複雑なオブジェクトに非常に適しています。しかし、属性として反復可能なタイプの組み込みを含有するもののために、のようなstrlistsetまたは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あなたは本当に正しいです。どうしてそのように書こうと思ったのかわかりません。おそらく、より多くのイテレータ構文の観点からイテレータ構文の働きを説明しようとする答えで、混乱を避けようとしていたのかもしれません。
John Strood

3

これは、なしの反復可能な関数yieldです。iter関数と、listPython 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

私は常にtwo-argの巧妙な使用に感謝しiterていますが、明確にするために:これは、yieldベースのジェネレーター関数を使用するよりも複雑で効率が悪いです。Pythonには、yieldここでは利用できないベースジェネレーター関数に対する多数のインタープリターサポートがあり、このコードを大幅に遅くしています。それでも賛成投票。
ShadowRanger 2018

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)
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.