ホームページ  >  記事  >  バックエンド開発  >  Python でのイテレータとジェネレータの高度な使用法

Python でのイテレータとジェネレータの高度な使用法

高洛峰
高洛峰オリジナル
2017-03-01 14:09:141120ブラウズ

イテレータ

イテレータは、反復プロトコルに準拠するオブジェクトです。基本的には、呼び出されるとシーケンス内の次の項目を返す next メソッドがあることを意味します。返す項目がない場合は、StopIteration 例外を発生させます。

反復可能なオブジェクトでは 1 つのループが許可されます。単一の反復の状態 (位置) が保持されます。別の観点から見ると、シーケンスがループされるたびに反復オブジェクトが必要になります。これは、同じシーケンスを複数回反復できることを意味します。反復ロジックをシーケンスから分離すると、反復する方法がさらに増えます。

コンテナの __iter__ メソッドを呼び出してイテレータ オブジェクトを作成するのが、イテレータをマスターする最も直接的な方法です。 iter 関数を使用すると、キーストロークがいくつか節約されます。

>>> nums = [1,2,3]   # note that ... varies: these are different objects
>>> iter(nums)              
<listiterator object at ...>
>>> nums.__iter__()           
<listiterator object at ...>
>>> nums.__reversed__()         
<listreverseiterator object at ...>

>>> it = iter(nums)
>>> next(it)      # next(obj) simply calls obj.next()
1
>>> it.next()
2
>>> next(it)
3
>>> next(it)
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
StopIteration

ループ内で使用すると、StopIteration が受け入れられ、ループが停止します。しかし、明示的な呼び出しでは、イテレータ要素が使い果たされると、それにアクセスすると例外がスローされることがわかります。

for...in ループを使用し、__iter__ メソッドも使用します。これにより、透過的にシーケンスの反復を開始できるようになります。しかし、すでに反復子がある場合は、それらを for ループ内で同様に使用できるようにしたいと考えます。これを実現するために、イテレータには next に加えて、イテレータ自体 (self) を返すメソッド __iter__ も用意されています。

Python のイテレーターのサポートは広く普及しています。標準ライブラリ内のすべてのシーケンスおよび順序なしコンテナーがサポートされています。この概念は他のものにも拡張されています。たとえば、ファイル オブジェクトは行の反復をサポートします。

>>> f = open(&#39;/etc/fstab&#39;)
>>> f is f.__iter__()
True

ファイル自体は反復子であり、その __iter__ メソッドは別個のオブジェクトを作成しません。シングルスレッドの順次読み取りのみが許可されます。

式の生成
反復可能オブジェクトを作成する 2 番目の方法は、リスト理解の基礎であるジェネレーター式を使用することです。わかりやすくするために、生成された式は常に括弧または式で囲まれます。括弧を使用すると、ジェネレーター反復子が作成されます。角括弧の場合、このプロセスは「短絡」され、リストが取得されます。

>>> (i for i in nums)          
<generator object <genexpr> at 0x...>
>>> [i for i in nums]
[1, 2, 3]
>>> list(i for i in nums)
[1, 2, 3]

Python 2.7 および 3.x では、リスト式の構文が辞書式とセット式に拡張されました。生成された式が中かっこで囲まれると、セットが作成されます。辞書辞書は、式に key:value の形式のキーと値のペアが含まれている場合に作成されます:

>>> {i for i in range(3)}  
set([0, 1, 2])
>>> {i:i**2 for i in range(3)}  
{0: 0, 1: 1, 2: 4}

不幸にして古いバージョンの Python に囚われてしまった場合、この構文は少し悪いです:

>>> set(i for i in &#39;abc&#39;)
set([&#39;a&#39;, &#39;c&#39;, &#39;b&#39;])
>>> dict((i, ord(i)) for i in &#39;abc&#39;)
{&#39;a&#39;: 97, &#39;c&#39;: 99, &#39;b&#39;: 98}

生成 この式は非常に単純なので、これ以上の説明は必要ありません。言及する価値のある落とし穴が 1 つだけあります。それは、Python のバージョン 3 未満ではインデックス変数 (i) がリークすることです。

ジェネレーター

ジェネレーターは、単一の値ではなく結果のリストを生成する関数です。

反復可能オブジェクトを作成する 3 番目の方法は、ジェネレーター関数を呼び出すことです。ジェネレーターは、yield キーワードを含む関数です。このキーワードが存在するだけで関数の性質が完全に変わることは注目に値します。yield ステートメントを呼び出す必要はなく、アクセスする必要さえありません。ただし、関数をジェネレーターにしてみましょう。関数が呼び出されると、その関数内の命令が実行されます。そして、ジェネレーターが呼び出されると、その中の最初の命令の前で実行が停止します。ジェネレーターを呼び出すと、反復プロトコルに関連付けられたジェネレーター オブジェクトが作成されます。通常の関数と同様に、同時呼び出しと再帰呼び出しが許可されます。
next が呼び出されると、関数は最初の yield まで実行されます。 yield ステートメントが出現するたびに、next として返される値が取得され、yield ステートメントが実行された後、関数の実行が停止されます。

>>> def f():
...  yield 1
...  yield 2
>>> f()                  
<generator object f at 0x...>
>>> gen = f()
>>> gen.next()
1
>>> gen.next()
2
>>> gen.next()
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
StopIteration

単一のジェネレーター関数呼び出しの過程全体を見てみましょう。

>>> def f():
...  print("-- start --")
...  yield 3
...  print("-- middle --")
...  yield 4
...  print("-- finished --")
>>> gen = f()
>>> next(gen)
-- start --
3
>>> next(gen)
-- middle --
4
>>> next(gen)              
-- finished --
Traceback (most recent call last):
 ...
StopIteration

通常の関数で f() を実行して即座に print を実行するのに比べ、関数本体内のステートメントを実行せずに gen が代入されます。次に gen.next() が呼び出されたときのみ、最初の yield 部分までのステートメントが実行されます。 2 番目のステートメントは -- middle -- を出力し、2 番目の yield に達すると実行を停止します。 3 番目の次の print--finished-- そして関数の最後まで、yield がないため、例外がスローされます。

関数 yield の後に制御が呼び出し元に戻るとどうなりますか?各ジェネレーターの状態はジェネレーター オブジェクトに保存されます。この時点から、ジェネレーター関数は別のスレッドで実行されているように見えますが、これは単なる錯覚です。実行は厳密にシングルスレッドですが、インタープリターは次の値リクエストの間の状態を保持し、保存します。

ジェネレーターはなぜ便利ですか?イテレータのセクションで強調したように、ジェネレータ関数は反復可能なオブジェクトを作成する別の方法にすぎません。 yield ステートメントで完了できるものはすべて、次のメソッドでも完了できます。ただし、関数を使用してインタプリタが魔法のようにイテレータを作成できるようにすることには利点があります。関数は、next メソッドと __iter__ メソッドを必要とするクラス定義よりもはるかに短くすることができます。さらに重要なことは、ジェネレーターの作成者は、連続する次の呼び出しの間に反復子オブジェクトのインスタンス プロパティを渡す必要があるよりも、ローカル変数にローカライズされたステートメントを簡単に理解できることです。

还有问题是为何迭代器有用?当一个迭代器用来驱动循环,循环变得简单。迭代器代码初始化状态,决定是否循环结束,并且找到下一个被提取到不同地方的值。这凸显了循环体——最值得关注的部分。除此之外,可以在其它地方重用迭代器代码。

双向通信
每个yield语句将一个值传递给调用者。这就是为何PEP 255引入生成器(在Python2.2中实现)。但是相反方向的通信也很有用。一个明显的方式是一些外部(extern)语句,或者全局变量或共享可变对象。通过将先前无聊的yield语句变成表达式,直接通信因PEP 342成为现实(在2.5中实现)。当生成器在yield语句之后恢复执行时,调用者可以对生成器对象调用一个方法,或者传递一个值 给 生成器,然后通过yield语句返回,或者通过一个不同的方法向生成器注入异常。

第一个新方法是send(value),类似于next(),但是将value传递进作为yield表达式值的生成器中。事实上,g.next()和g.send(None)是等效的。

第二个新方法是throw(type, value=None, traceback=None),等效于在yield语句处

raise type, value, traceback

不像raise(从执行点立即引发异常),throw()首先恢复生成器,然后仅仅引发异常。选用单次throw就是因为它意味着把异常放到其它位置,并且在其它语言中与异常有关。

当生成器中的异常被引发时发生什么?它可以或者显式引发,当执行某些语句时可以通过throw()方法注入到yield语句中。任一情况中,异常都以标准方式传播:它可以被except和finally捕获,或者造成生成器的中止并传递给调用者。

因完整性缘故,值得提及生成器迭代器也有close()方法,该方法被用来让本可以提供更多值的生成器立即中止。它用生成器的__del__方法销毁保留生成器状态的对象。

让我们定义一个只打印出通过send和throw方法所传递东西的生成器。

>>> import itertools
>>> def g():
...   print &#39;--start--&#39;
...   for i in itertools.count():
...     print &#39;--yielding %i--&#39; % i
...     try:
...       ans = yield i
...     except GeneratorExit:
...       print &#39;--closing--&#39;
...       raise
...     except Exception as e:
...       print &#39;--yield raised %r--&#39; % e
...     else:
...       print &#39;--yield returned %s--&#39; % ans

>>> it = g()
>>> next(it)
--start--
--yielding 0--
0
>>> it.send(11)
--yield returned 11--
--yielding 1--
1
>>> it.throw(IndexError)
--yield raised IndexError()--
--yielding 2--
2
>>> it.close()
--closing--

注意: next还是__next__?

在Python 2.x中,接受下一个值的迭代器方法是next,它通过全局函数next显式调用,意即它应该调用__next__。就像全局函数iter调用__iter__。这种不一致在Python 3.x中被修复,it.next变成了it.__next__。对于其它生成器方法——send和throw情况更加复杂,因为它们不被解释器隐式调用。然而,有建议语法扩展让continue带一个将被传递给循环迭代器中send的参数。如果这个扩展被接受,可能gen.send会变成gen.__send__。最后一个生成器方法close显然被不正确的命名了,因为它已经被隐式调用。

链式生成器
注意: 这是PEP 380的预览(还未被实现,但已经被Python3.3接受)

比如说我们正写一个生成器,我们想要yield一个第二个生成器——一个子生成器(subgenerator)——生成的数。如果仅考虑产生(yield)的值,通过循环可以不费力的完成:

subgen = some_other_generator()
for v in subgen:
  yield v

然而,如果子生成器需要调用send()、throw()和close()和调用者适当交互的情况下,事情就复杂了。yield语句不得不通过类似于前一章节部分定义的try...except...finally结构来保证“调试”生成器函数。这种代码在PEP 380中提供,现在足够拿出将在Python 3.3中引入的新语法了:

yield from some_other_generator()

像上面的显式循环调用一样,重复从some_other_generator中产生值直到没有值可以产生,但是仍然向子生成器转发send、throw和close。

更多Python中的迭代器与生成器高级用法相关文章请关注PHP中文网!

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。