Python 3のItertoolsの例

Python 3のItertoolsの例

"gem "およびhttp://jmduke.com/posts/a-gentle-introduction-to-itertools/と呼ばれています。 [「これまでで最もクールなこと」]そして聞いたことがないなら、Python 3標準ライブラリの最大のコーナーの1つである `+ itertools +`を見逃しています。

`+ itertools +`モジュールで利用可能な機能を学習するための優れたリソースがいくつかあります。 docs自体が出発点として最適です。 https://pymotw.com/3/itertools/index.html [この投稿]も同様です。

しかし、 `+ itertools +`についてのことは、含まれている関数の定義を知るだけでは十分ではないということです。 本当の力は、これらの関数を構成して、高速でメモリ効率の良い見栄えの良いコードを作成することにあります。

この記事のアプローチは異なります。 「+ itertools +」を一度に1つの関数で紹介するのではなく、「繰り返し考える」ことを奨励するための実用的な例を構築します。一般に、例は単純に始まり、徐々に複雑さが増します。

警告:この記事は長く、中級から上級のPythonプログラマーを対象としています。 飛び込む前に、Python 3でのイテレーターとジェネレーターの使用、複数の割り当て、およびタプルのアンパックに自信を持つ必要があります。 そうでない場合、または知識を磨く必要がある場合は、本を読む前に以下を確認することを検討してください。

無料ボーナス: こちらをクリックしてitertoolsチートシートを入手してくださいに、このチュートリアルで示したテクニックがまとめられています。

準備完了? 良い旅のあり方を、質問から始めましょう。

`+ Itertools +`とは何ですか、なぜ使用する必要があるのですか?

https://docs.python.org/3/library/itertools.html [+ itertools + docs]によれば、それは「APL、Haskellからのコンストラクトに触発された多くのイテレータービルディングブロックを実装するモジュール[です]」 、およびSML …​一緒に、「イテレータ代数」を形成し、純粋なPythonで特殊なツールを簡潔かつ効率的に構築できるようにします。」

大ざっぱに言えば、これは、 `+ itertools `の関数が反復子を「操作」して、より複雑な反復子を生成することを意味します。 たとえば、https://docs.python.org/3/library/functions.html#zip [ビルトイン ` zip()+`関数]を考えてみましょう。これは、任意の数の反復可能要素を引数として受け取り、対応する要素のタプルの反復子:

>>>

>>> list(zip([1, 2, 3], ['a', 'b', 'c']))
[(1, 'a'), (2, 'b'), (3, 'c')]

正確には、どのように `+ zip()+`が機能しますか?

`+ [1、2、3] `および ` ['a'、 'b'、 'c'] `は、すべてのリストと同様に反復可能です。つまり、要素を1つずつ返すことができます。 技術的には、 ` . iter ()`または ` . getitem ()+`メソッドを実装するPythonオブジェクトは反復可能です。 (詳細な説明については、https://docs.python.org/3/glossary.html#term-iterable [Python 3 docs用語集]を参照してください。)

https://docs.python.org/3/library/functions.html#iter [`+ iter()+`組み込み関数]は、イテラブルで呼び出されると、https://docs.pythonを返します。 org/3/library/stdtypes.html#typeiter [iterator object]その反復可能の場合:

>>>

>>> iter([1, 2, 3, 4])
<list_iterator object at 0x7fa80af0d898>

基本的に、 `+ zip()`関数は、基本的に、各引数で ` iter()`を呼び出し、 ` iter()`によって返された各イテレータを ` nextで進めることにより機能します。 ()+ `および結果をタプルに集約します。 `+ zip()+`によって返されるイテレータはこれらのタプルを繰り返し処理します。

https://docs.python.org/3/library/functions.html#map [`+ map()+`組み込み関数]はもう1つの「イテレータ演算子」であり、最も単純な形式では、一度に反復可能な1つの要素の各要素に対するパラメーター関数:

>>>

>>> list(map(len, ['abc', 'de', 'fghi']))
[3, 2, 4]

`+ map()`関数は、2番目の引数で ` iter()`を呼び出し、イテレータが使い果たされるまで ` next()`でこのイテレータを進め、最初の引数に渡された関数を適用することで機能します各ステップで ` next()`によって返される値に。 上記の例では、リスト内の各文字列の長さにわたる反復子を返すために、 ` ['abc'、 'de'、 'fghi'] `の各要素に対して ` len()+`が呼び出されます。

https://docs.python.org/3/glossary.html#term-iterator [反復子は反復可能]なので、 `+ zip()`と ` map()+`を組み合わせて、組み合わせで反復子を生成できます。複数の反復可能な要素の要素。 たとえば、次は2つのリストの対応する要素を合計します。

>>>

>>> list(map(sum, zip([1, 2, 3], [4, 5, 6])))
[5, 7, 9]

これは、「+ itertools 」の関数が「イテレータ代数」を形成することを意味します。 ` itertools +`は、上記の例のような特殊な「データパイプライン」を形成するために組み合わせることができるビルディングブロックのコレクションとして最適に表示されます。

_ 履歴ノート: Python 2では、組み込みのhttps://docs.python.org/2/library/functions.html#zip [+ zip()+]およびhttps://docs.python。 org/2/library/functions.html#map [+ map()+]関数はイテレータではなくリストを返します。 イテレータを返すには、https://docs.python.org/2/library/itertools.html#itertools.izip [+ izip()+]およびhttps://docs.python.org/2/library/itertools.html#itertools.imap[+imap()+ `] + itertools + の関数を使用する必要があります。 Python 3では、 `+ izip()+`と `+ imap()+`はhttps://docs.python.org/3.0/whatsnew/3.0.html#views-and-iterators-instead-of-listsになりました[+ itertools `から削除]および ` zip()`および ` map()`ビルトインを置き換えました。 ですから、ある意味では、Python 3で ` zip()`または ` map()`を使用したことがあるなら、すでに ` itertools +`を使用していることになります! _

このような「反復代数」が役立つ主な理由は2つあります。メモリ効率の向上(https://en.wikipedia.org/wiki/Lazy_evaluation[lazy evaluation]を使用)と実行時間の短縮です。 これを確認するには、次の問題を考慮してください。

_ 値のリスト「+ inputs 」と正の整数「 n 」を指定して、「 inputs 」を長さ「 n 」のグループに分割する関数を記述します。 簡単にするために、入力リストの長さは「 n 」で割り切れると仮定します。 たとえば、 ` inputs = [1、2、3、4、5、6] `と ` n = 2 `の場合、関数は ` [(1、2)、(3、4) 、(5、6)] + `。 _

素朴なアプローチをとると、次のように書くことができます。

def naive_grouper(inputs, n):
    num_groups = len(inputs)//n
    return [tuple(inputs[i*n:(i+1)*n]) for i in range(num_groups)]

テストすると、期待どおりに機能することがわかります。

>>>

>>> nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> naive_grouper(nums, 2)
[(1, 2), (3, 4), (5, 6), (7, 8), (9, 10)]

たとえば1億個の要素を含むリストを渡そうとするとどうなりますか? 大量のメモリが必要です! 十分なメモリを使用できる場合でも、出力リストが表示されるまでプログラムはしばらくハングします。

これを確認するには、以下を `+ naive.py +`というスクリプトに保存します:

def naive_grouper(inputs, n):
    num_groups = len(inputs)//n
    return [tuple(inputs[i*n:(i+1)*n]) for i in range(num_groups)]


for _ in naive_grouper(range(100000000), 10):
    pass

コンソールから、 `+ time +`コマンド(UNIXシステムの場合)を使用して、メモリ使用量とCPUユーザー時間を測定できます。 以下を実行する前に、少なくとも5GBの空きメモリがあることを確認してください:

$ time -f "Memory used (kB): %M\nUser time (seconds): %U" python3 naive.py
Memory used (kB): 4551872
User time (seconds): 11.04

_ *注意: *Ubuntuでは、上記の例が機能するためには、 `+ time `の代わりに `/usr/bin/time +`を実行する必要がある場合があります。 _

`+ naive_grouper()`の ` list `および ` tuple `実装では、 ` range(100000000)+`を処理するために約4.5GBのメモリが必要です。 イテレータを使用すると、この状況が大幅に改善されます。 次の点を考慮してください。

def better_grouper(inputs, n):
    iters = [iter(inputs)]* n
    return zip(*iters)

この小さな機能には多くのことが行われているので、具体的な例を挙げて説明しましょう。 式「+ [iters(inputs)] *n 」は、同じイテレータへの「 n +」参照のリストを作成します。

>>>

>>> nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> iters = [iter(nums)]* 2
>>> list(id(itr) for itr in iters)  # IDs are the same.
[139949748267160, 139949748267160]

次に、 `+ zip( *iters)`は、 ` iters `の各反復子の対応する要素のペアに対する反復子を返します。 最初の要素である「+1」が「最初の」イテレータから取得されると、「2番目」のイテレータは「2」で始まるようになりました。ワンステップ。 したがって、 `+ zip()`によって生成される最初のタプルは `(1、2)+`です。

この時点で、「+ iters 」の「両方」のイテレータは「+3」で始まるため、「+ zip()」が「最初の」イテレータから「+3」をプルすると、「+ 4+」が取得されます。 「」からタプル「(3、4)」を生成します。 このプロセスは、 ` zip()`が最終的に `(9、10)`を生成し、 ` iters +`の「両方」の反復子が使い果たされるまで続きます。

>>>

>>> nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> list(better_grouper(nums, 2))
[(1, 2), (3, 4), (5, 6), (7, 8), (9, 10)]

`+ better_grouper()`関数はいくつかの理由で優れています。 最初に、 ` len()`ビルトインへの参照なしで、 ` better_grouper()`は引数としてイテレータを使用できます(無限イテレータでさえも)。 第二に、リストではなくイテレータを返すことにより、 ` better_grouper()+`は膨大なイテラブルを問題なく処理でき、メモリの使用量を大幅に削減できます。

以下を `+ better.py `というファイルに保存し、コンソールから ` time +`で再度実行します。

def better_grouper(inputs, n):
    iters = [iter(inputs)]* n
    return zip(*iters)


for _ in better_grouper(range(100000000), 10):
    pass
$ time -f "Memory used (kB): %M\nUser time (seconds): %U" python3 better.py
Memory used (kB): 7224
User time (seconds): 2.48

これは、4分の1未満の時間で「+ naive.py +」よりも630倍少ないメモリ使用量です!

`+ itertools `が何であるか( "イテレータ代数")とそれを使用する理由(メモリ効率の向上と実行時間の短縮)を確認したので、 ` better_grouper()`を次のレベルは ` itertools +`です。

`+ grouper +`レシピ

`+ better_grouper()+`の問題は、2番目の引数に渡された値が最初の引数の反復可能要素の長さの要因ではない状況を処理しないことです。

>>>

>>> nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> list(better_grouper(nums, 4))
[(1, 2, 3, 4), (5, 6, 7, 8)]

グループ化された出力から要素9と10が欠落しています。 これは、渡された最短のイテラブルが使い果たされると、 `+ zip()+`が要素の集約を停止するために発生します。 9と10を含む3番目のグループを返す方が理にかなっています。

これを行うには、 `+ itertools.zip_longest()`を使用できます。 この関数は、引数として任意の数の反復可能要素と、デフォルトで「 None 」に設定される「 fillvalue 」キーワード引数を受け入れます。 ` zip()`と ` zip_longest()+`の違いを理解する最も簡単な方法は、出力例をいくつか見ることです:

>>>

>>> import itertools as it
>>> x = [1, 2, 3, 4, 5]
>>> y = ['a', 'b', 'c']
>>> list(zip(x, y))
[(1, 'a'), (2, 'b'), (3, 'c')]
>>> list(it.zip_longest(x, y))
[(1, 'a'), (2, 'b'), (3, 'c'), (4, None), (5, None)]

これを念頭に置いて、「+ better_grouper()」の「 zip()」を「 zip_longest()+」に置き換えます。

import itertools as it


def grouper(inputs, n, fillvalue=None):
    iters = [iter(inputs)] * n
    return it.zip_longest(*iters, fillvalue=fillvalue)

これで、より良い結果が得られます。

>>>

>>> nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> print(list(grouper(nums, 4)))
[(1, 2, 3, 4), (5, 6, 7, 8), (9, 10, None, None)]

`+ grouper()`関数は、 ` itertools `ドキュメントのhttps://docs.python.org/3.6/library/itertools.html#itertools-recipes[Recipes section]にあります。 レシピは、 ` itertools +`を有利に使用する方法の優れたインスピレーションの源です。

注意:これ以降、例の先頭に「+ import itertools as it 」という行は含まれません。 コード例のすべての ` itertools `メソッドは、 ` it。+`で始まります。モジュールのインポートが暗黙的に示されています。

このチュートリアルの例を実行するときに、「+ NameError:name 'itertools' is notdefined + 」または「+ NameError:name 'it' not defined +」例外が発生した場合は、「+ itertools + `最初のモジュール。

Et tu、ブルートフォース?

一般的なインタビュー形式の問題は次のとおりです。

_ 20ドル札が3つ、10ドル札が5つ、5ドル札が2つ、1ドル札が5つあります。 100ドルの請求書を変更する方法はいくつありますか? _

"brute force "この問題を解決するには、ウォレットから1つの請求書を選択する方法をリストアップし、これらのいずれかが変更を加えるかどうかを確認します100ドル、ウォレットから2つの請求書を選択する方法をリストし、再度確認するなど。

しかし、あなたはプログラマなので、当然このプロセスを自動化する必要があります。

最初に、ウォレットにある請求書のリストを作成します。

bills = [20, 20, 20, 10, 10, 10, 10, 10, 5, 5, 1, 1, 1, 1, 1]

一連の_n_項目から_k_項目を選択することはhttps://en.wikipedia.org/wiki/Combination[combination]と呼ばれ、 `+ itertools `はここに戻ってきます。 ` itertools.combinations()`関数は、反復可能な ` inputs `と正の整数 ` n `の2つの引数を取り、 ` inputs `の ` n +`要素のすべての組み合わせのタプルに対する反復子を生成します。

たとえば、ウォレットの3つの請求書の組み合わせをリストするには、次のようにします。

>>>

>>> list(it.combinations(bills, 3))
 [(20, 20, 20), (20, 20, 10), (20, 20, 10), ... ]

この問題を解決するには、1から `+ len(bills)+`の正の整数をループし、各サイズの組み合わせが$ 100になることを確認します。

>>>

>>> makes_100 = []
>>> for n in range(1, len(bills) + 1):
...     for combination in it.combinations(bills, n):
...         if sum(combination) == 100:
...             makes_100.append(combination)

`+ makes_100 `を出力すると、多くの組み合わせが繰り返されることに気付くでしょう。 これは3ドルの20ドル札と4ドルの10ドル札で100ドルで変更できますが、 ` combinations()+`はウォレットの最初の4ドルの10ドル札でこれを行います。最初、3番目、4番目、5番目の10ドルの請求書。最初、2番目、4番目、5番目の10ドルの請求書。等々。

`+ makes_100 `から重複を削除するには、それを ` set +`に変換します:

>>>

>>> set(makes_100)
{(20, 20, 10, 10, 10, 10, 10, 5, 1, 1, 1, 1, 1),
 (20, 20, 10, 10, 10, 10, 10, 5, 5),
 (20, 20, 20, 10, 10, 10, 5, 1, 1, 1, 1, 1),
 (20, 20, 20, 10, 10, 10, 5, 5),
 (20, 20, 20, 10, 10, 10, 10)}

だから、あなたの財布に持っている請求書で100ドルの請求書に変更を加えるための5つの方法があります。

同じ問題のバリエーションを次に示します。

_ 50ドル、20ドル、10ドル、5ドル、1ドルの請求書を使用して、100ドルの請求書を変更する方法はいくつありますか? _

この場合、請求書のコレクションは事前に設定されていないため、任意の数の請求書を使用してすべての可能な組み合わせを生成する方法が必要です。 これには、 `+ itertools.combinations_with_replacement()+`関数が必要です。

これは、 `+ combinations()`と同じように機能し、反復可能な ` inputs `と正の整数 ` n `を受け入れ、 ` inputs `の要素の ` n `タプルに対する反復子を返します。 違いは、 ` combinations_with_replacement()+`が返すタプルで要素を繰り返すことができることです。

例えば:

>>>

>>> list(it.combinations_with_replacement([1, 2], 2))
[(1, 1), (1, 2), (2, 2)]

それを `+ combinations()+`と比較してください:

>>>

>>> list(it.combinations([1, 2], 2))
[(1, 2)]

修正された問題の解決策は次のとおりです。

>>>

>>> bills = [50, 20, 10, 5, 1]
>>> make_100 = []
>>> for n in range(1, 101):
...     for combination in it.combinations_with_replacement(bills, n):
...         if sum(combination) == 100:
...             makes_100.append(combination)

この場合、 `+ combinations_with_replacement()+`は何も生成しないため、重複を削除する必要はありません。

>>>

>>> len(makes_100)
343

上記のソリューションを実行すると、出力が表示されるまでに時間がかかることがあります。 96,560,645の組み合わせを処理する必要があるためです!

もう1つの「ブルートフォース」の `+ itertools `関数は、 ` permutations()+`です。これは、単一の反復可能要素を受け入れ、その要素のすべての可能な順列(再配置)を生成します。

>>>

>>> list(it.permutations(['a', 'b', 'c']))
[('a', 'b', 'c'), ('a', 'c', 'b'), ('b', 'a', 'c'),
 ('b', 'c', 'a'), ('c', 'a', 'b'), ('c', 'b', 'a')]

3つの要素のイテラブルは6つの順列を持ち、より長いイテラブルの順列の数は非常に速く増加します。 実際、長さ_n_の反復可能オブジェクトには_n!_順列があります。

これを概観するために、_n = 1_から_n = 10_のこれらの数値の表を次に示します。

n n!

2

2

3

6

4

24

5

120

6

720

7

5,040

8

40,320

9

362,880

10

3,628,800

少数の入力が多数の結果を生成する現象はhttps://en.wikipedia.org/wiki/Combinatorial_explosion[combinatorial explosion]と呼ばれ、 + combinations()+`を使用する場合に留意する必要があるものです。 、 `+ combinations_with_replacement()+、および + permutations()+

通常、ブルートフォースアルゴリズムを使用しないことをお勧めしますが、使用する必要がある場合もあります(たとえば、アルゴリズムの正確性が重要な場合、または考えられるすべての結果を考慮する必要がある場合)。 その場合、 `+ itertools +`でカバーします。

セクションの要約

このセクションでは、3つの + itertools +`関数に遭遇しました: `+ combinations()++ combinations_with_replacement()+、および + permutations()+

次に進む前に、これらの機能を確認しましょう。

`+ itertools.combinations +`の例

_ _ + combinations(iterable、n)+

iterableの要素の長さnの連続した組み合わせを返します。 _ _

>>>

>>> combinations([1, 2, 3], 2)
(1, 2), (1, 3), (2, 3)
`+ itertools.combinations_with_replacement +`の例

_ _ + combinations_with_replacement(iterable、n)+

iterable内の要素の連続するn個の長さの組み合わせを返し、個々の要素が連続して繰り返されるようにします。 _ _

>>>

>>> combinations_with_replacement([1, 2], 2)
(1, 1), (1, 2), (2, 2)
`+ itertools.permutations +`の例

_ _ + permutations(iterable、n = None)+

iterable内の要素の連続するn長の順列を返します。 _ _

>>>

>>> permutations('abc')
('a', 'b', 'c'), ('a', 'c', 'b'), ('b', 'a', 'c'),
('b', 'c', 'a'), ('c', 'a', 'b'), ('c', 'b', 'a')

数字のシーケンス

`+ itertools +`を使用すると、無限シーケンス上でイテレータを簡単に生成できます。 このセクションでは、数値シーケンスを調べますが、ここで見られるツールと手法は決して数値に限定されません。

偶数とオッズ

最初の例では、偶数の整数と奇数の整数に対する反復子のペアを作成します。明示的に算術を実行することはありません。_飛び込む前に、ジェネレーターを使用した算術ソリューションを見てみましょう。

>>>

>>> def evens():
...     """Generate even integers, starting with 0."""
...     n = 0
...     while True:
...         yield n
...         n += 2
...
>>> evens = evens()
>>> list(next(evens) for _ in range(5))
[0, 2, 4, 6, 8]

>>> def odds():
...     """Generate odd integers, starting with 1."""
...     n = 1
...     while True:
...         yield n
...         n += 2
...
>>> odds = odds()
>>> list(next(odds) for _ in range(5))
[1, 3, 5, 7, 9]

これは非常に簡単ですが、 `+ itertools `を使用すると、これをよりコンパクトに実行できます。 必要な関数は ` itertools.count()+`で、これはまさにその通りに機能します:カウントします。デフォルトでは数値0から始まります。

>>>

>>> counter = it.count()
>>> list(next(counter) for _ in range(5))
[0, 1, 2, 3, 4]

`+ start `キーワード引数(デフォルトは0)を設定することで、好きな数からカウントを開始できます。 ` step `キーワード引数を設定して、 ` count()+`から返される数値の間隔を決定することもできます。デフォルトは1です。

`+ count()+`を使用すると、偶数および奇数の整数に対する反復子はリテラルの1ライナーになります。

>>>

>>> evens = it.count(step=2)
>>> list(next(evens) for _ in range(5))
[0, 2, 4, 6, 8]

>>> odds = it.count(start=1, step=2)
>>> list(next(odds) for _ in range(5))
[1, 3, 5, 7, 9]

Python 3.1以降では、 `+ count()+`関数は整数以外の引数も受け入れます。

>>>

>>> count_with_floats = it.count(start=0.5, step=0.75)
>>> list(next(count_with_floats) for _ in range(5))
[0.5, 1.25, 2.0, 2.75, 3.5]

負の数を渡すこともできます:

>>>

>>> negative_count = it.count(start=-1, step=-0.5)
>>> list(next(negative_count) for _ in range(5))
[-1, -1.5, -2.0, -2.5, -3.0]

ある意味では、 `+ count()`は組み込みの ` range()`関数に似ていますが、 ` count()+`は常に無限のシーケンスを返します。 完全に繰り返し処理することは不可能なので、無限のシーケンスはどのようなものか疑問に思うかもしれません。 それは有効な質問であり、無限イテレータに初めて触れたときは認めますが、私もその点をよく知りませんでした。

無限イテレータの威力を実感させた例は次のとおりで、https://docs.python.org/3/library/functions.html#enumerate [built-in `+ enumerate()の動作をエミュレートします+ `関数]:

>>>

>>> list(zip(it.count(), ['a', 'b', 'c']))
[(0, 'a'), (1, 'b'), (2, 'c')]

これは単純な例ですが、考えてみてください。`+ for + `ループを使用せず、リストの長さを事前に知らずにリストを列挙しただけです。

再発関係

recurrence relationは、再帰的な数式を使用して一連の数値を記述する方法です。 最もよく知られている繰り返し関係の1つは、https://en.wikipedia.org/wiki/Fibonacci_number [Fibonacci sequence]を記述するものです。

フィボナッチ数列は「+0、1、1、2、3、5、8、13、…​ +」という数列です。 0と1で始まり、シーケンス内の後続の各番号は、前の2つの合計です。 このシーケンスの数字は、フィボナッチ数と呼ばれます。 数学表記では、_n_番目のフィボナッチ数を記述する再帰関係は次のようになります。

Fibonacci Recurrence Relation、width = 1269、height = 73

_ 注意: Googleを検索すると、Pythonでこれらの数値の実装が多数見つかります。 Real Pythonに関するhttps://realpython.com/python-thinking-recursively/#naive-recursion-is-naive[Pythonで再帰的に考える]の記事で、それらを生成する再帰関数を見つけることができます。 _

ジェネレーターで生成されたフィボナッチ数列を見るのは一般的です:

def fibs():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

フィボナッチ数を記述する繰り返しリレーションは、_second order recurrence relation_と呼ばれます。これは、シーケンスの次の数を計算するために、その後ろの2つの数字を振り返る必要があるためです。

一般に、二次の再帰関係の形式は次のとおりです。

Second Order Recurrence Relation、width = 926、height = 76

ここで、P _、 Q_、および_R_は定数です。 シーケンスを生成するには、2つの初期値が必要です。 フィボナッチ数の場合、P = Q = 1、R = 0、初期値は0および1です。

ご想像のとおり、_最初の順序の関係_の形式は次のとおりです。

First Order Recurrence Relation、width = 589、height = 76

一次および二次の再帰関係によって記述できる数の無数のシーケンスがあります。 たとえば、正の整数は、P = Q = 1および初期値1の1次の回帰関係として記述できます。 偶数の整数の場合、P = 1および_Q_ = 2を初期値0で使用します。

このセクションでは、一次または二次の再帰関係で値を記述できる_any_シーケンスを生成するための関数を構築します。

一次回帰関係

「+ count()+」が非負整数、偶数整数、奇数整数のシーケンスを生成する方法をすでに見てきました。 また、シーケンス_3n = 0、3、6、9、12、…_および_4n = 0、4、8、12、16、…_を生成するために使用できます。

count_by_three = it.count(step=3)
count_by_four = it.count(step=4)

実際、 `+ count()+`は、希望する任意の数の倍数のシーケンスを生成できます。 これらのシーケンスは、1次の再帰関係で記述できます。 たとえば、ある数_n_の倍数のシーケンスを生成するには、P = 1、Q = n、および初期値0を使用します。

1次再帰関係のもう1つの簡単な例は、定数シーケンス_n、n、n、n、n…です。ここで、_n_は任意の値です。 このシーケンスでは、_P = 1および_Q_ = 0に初期値_n_を設定します。 `+ itertools `は、 ` repeat()+`関数を使用して、このシーケンスを簡単に実装する方法も提供します。

all_ones = it.repeat(1)  # 1, 1, 1, 1, ...
all_twos = it.repeat(2)  # 2, 2, 2, 2, ...

繰り返し値の有限シーケンスが必要な場合は、2番目の引数として正の整数を渡すことにより、停止点を設定できます。

five_ones = it.repeat(1, 5)  # 1, 1, 1, 1, 1
three_fours = it.repeat(4, 3)  # 4, 4, 4

それほど明白ではないかもしれないのは、1と-1を交互に繰り返すシーケンス「+ 1、-1、1、-1、-1、-1、…​ +」も1次の再帰関係で記述できることです。 P = -1、Q = 0、初期値1を取ります。

`+ itertools.cycle()`関数を使用してこのシーケンスを生成する簡単な方法があります。 この関数は、引数として反復可能な ` inputs `を受け取り、 ` inputs `の値に無限のイテレータを返します。これは、 ` inputs +`の終わりに達すると先頭に戻ります。 したがって、1と-1の交互のシーケンスを生成するには、次のようにします。

alternating_ones = it.cycle([1, -1])  # 1, -1, 1, -1, 1, -1, ...

ただし、このセクションの目的は、任意の1次再帰関係を生成できる単一の関数を作成することです。P、_ Q_、および初期値を渡すだけです。 これを行う1つの方法は、 `+ itertools.accumulate()+`を使用することです。

+ accumulate()+`関数は、反復可能な `+ inputs +`と binary function `+ func +(つまり、正確に2つの入力を持つ関数)の2つの引数を取り、適用の累積結果に対する反復子を返します`+ func `から ` inputs +`の要素へ。 次のジェネレーターとほぼ同等です。

def accumulate(inputs, func):
    itr = iter(inputs)
    prev = next(itr)
    for cur in itr:
        yield prev
        prev = func(prev, cur)

例えば:

>>>

>>> import operator
>>> list(it.accumulate([1, 2, 3, 4, 5], operator.add))
[1, 3, 6, 10, 15]

+ accumulate()+`によって返されるイテレータの最初の値は、常に入力シーケンスの最初の値です。 上記の例では、これは1-+ [1、2、3、4、5] +`の最初の値です。

出力イテレータの次の値は、入力シーケンスの最初の2つの要素の合計です: + add(1、2)= 3 +。 次の値を生成するために、 `+ accumulate()`は ` add(1、2)+`の結果を受け取り、これを入力シーケンスの3番目の値に追加します。

add(3, 3) = add(add(1, 2), 3) = 6

`+ accumulate()`によって生成される4番目の値は、 ` add(add(add(1、2)、3)、4)= 10 +`などです。

`+ accumulate()`の2番目の引数のデフォルトは ` operator.add()+`であるため、前の例は次のように簡略化できます。

>>>

>>> list(it.accumulate([1, 2, 3, 4, 5]))
[1, 3, 6, 10, 15]

組み込みの `+ min()`を ` accumulate()+`に渡すと、実行中の最小値が追跡されます。

>>>

>>> list(it.accumulate([9, 21, 17, 5, 11, 12, 2, 6], min))
[9, 9, 9, 5, 5, 5, 2, 2]

より複雑な関数は、 `+ lambda `式を使用して ` accumulate()+`に渡すことができます。

>>>

>>> list(it.accumulate([1, 2, 3, 4, 5], lambda x, y: (x + y)/2))
[1, 1.5, 2.25, 3.125, 4.0625]

`+ accumulate()+`に渡されるバイナリ関数の引数の順序は重要です。 最初の引数は常に以前に累積された結果であり、2番目の引数は常に入力反復可能の次の要素です。 たとえば、次の式の出力の違いを考慮してください。

>>>

>>> list(it.accumulate([1, 2, 3, 4, 5], lambda x, y: x - y))
[1, -1, -4, -8, -13]

>>> list(it.accumulate([1, 2, 3, 4, 5], lambda x, y: y - x))
[1, 1, 2, 2, 3]

再帰関係をモデル化するには、 + accumulate()+`に渡されるバイナリ関数の2番目の引数を無視するだけです。 つまり、値 `+ p ++ q +、および `+ s `を指定すると、 ` lambda x、:p *s + q `は、_sᵢ_で定義された再帰関係で ` x `に続く値を返します。 =_Psᵢ₋₁_ _Q

`+ accumulate()`が結果の再帰関係を反復処理するために、正しい初期値を持つ無限シーケンスをそれに渡す必要があります。 初期値が再帰関係の初期値である限り、シーケンス内の残りの値が何であるかは関係ありません。 これを行うには、 ` repeat()+`を使用します。

def first_order(p, q, initial_val):
    """Return sequence defined by s(n) = p* s(n-1) + q."""
    return it.accumulate(it.repeat(initial_val), lambda s, _: p*s + q)

`+ first_order()+`を使用すると、上記のシーケンスを次のように構築できます。

>>> evens = first_order(p=1, q=2, initial_val=0)
>>> list(next(evens) for _ in range(5))
[0, 2, 4, 6, 8]

>>> odds = first_order(p=1, q=2, initial_val=1)
>>> list(next(odds) for _ in range(5))
[1, 3, 5, 7, 9]

>>> count_by_threes = first_order(p=1, q=3, initial_val=0)
>>> list(next(count_by_threes) for _ in range(5))
[0, 3, 6, 9, 12]

>>> count_by_fours = first_order(p=1, q=4, initial_val=0)
>>> list(next(count_by_fours) for _ in range(5))
[0, 4, 8, 12, 16]

>>> all_ones = first_order(p=1, q=0, initial_val=1)
>>> list(next(all_ones) for _ in range(5))
[1, 1, 1, 1, 1]

>>> all_twos = first_order(p=1, q=0, initial_val=2)
>>> list(next(all_twos) for _ in range(5))
[2, 2, 2, 2, 2]

>>> alternating_ones = first_order(p=-1, 0, initial_val=1)
>>> list(next(alternating_ones) for _ in range(5))
[1, -1, 1, -1, 1]
二次回帰関係

フィボナッチ数列のような2次の回帰関係で記述される数列の生成は、1次の回帰関係に使用されるものと同様の手法を使用して実現できます。

ここでの違いは、シーケンスの前の2つの要素を追跡するタプルの中間シーケンスを作成し、最後のシーケンスを取得するためにこれらの各タプルを最初のコンポーネントに「+ map()+」する必要があることです。

表示は次のとおりです。

def second_order(p, q, r, initial_values):
    """Return sequence defined by s(n) = p *s(n-1) + q* s(n-2) + r."""
    intermediate = it.accumulate(
        it.repeat(initial_values),
        lambda s, _: (s[1], p*s[1] + q*s[0] + r)
    )
    return map(lambda x: x[0], intermediate)

`+ second_order()+`を使用すると、次のようにフィボナッチ数列を生成できます。

>>> fibs = second_order(p=1, q=1, r=0, initial_values=(0, 1))
>>> list(next(fibs) for _ in range(8))
[0, 1, 1, 2, 3, 5, 8, 13]

他のシーケンスは、「+ p 」、「 q 」、および「 r +」の値を変更することで簡単に生成できます。 たとえば、https://en.wikipedia.org/wiki/Pell_number [Pell numbers]とhttps://en.wikipedia.org/wiki/Lucas_number[Lucas numbers]は次のように生成できます。

pell = second_order(p=2, q=1, r=0, initial_values=(0, 1))
>>> list(next(pell) for _ in range(6))
[0, 1, 2, 5, 12, 29]

>>> lucas = second_order(p=1, q=1, r=0, initial_values=(2, 1))
>>> list(next(lucas) for _ in range(6))
[2, 1, 3, 4, 7, 11]

交互のフィボナッチ数を生成することもできます:

>>> alt_fibs = second_order(p=-1, q=1, r=0, initial_values=(-1, 1))
>>> list(next(alt_fibs) for _ in range(6))
[-1, 1, -2, 3, -5, 8]

あなたが私のような巨大な数学オタクであれば、これは本当に素晴らしいことですが、少し前に戻って、このセクションの最初の `+ second_order()`を ` fibs()+`ジェネレータと比較してください。 どちらがわかりやすいですか?

これは貴重な教訓です。 `+ accumulate()+`関数はツールキットに含まれる強力なツールですが、使用する際に明快さと読みやすさを犠牲にすることを意味する場合があります。

セクションの要約

このセクションでいくつかの `+ itertools +`関数を見ました。 それらを今見直しましょう。

`+ itertools.count +`の例

_ _ + count(start = 0、step = 1)+

` + next ()+ `メソッドが連続した値を返すcountオブジェクトを返します。

_ _

>>>

>>> count()
0, 1, 2, 3, 4, ...

>>> count(start=1, step=2)
1, 3, 5, 7, 9, ...
`+ itertools.repeat +`の例

_ _ + repeat(object、times = 1)+

指定された回数だけオブジェクトを返すイテレータを作成します。 指定しない場合、オブジェクトを無限に返します。 _ _

>>>

>>> repeat(2)
2, 2, 2, 2, 2 ...

>>> repeat(2, 5)  # Stops after 5 repititions.
2, 2, 2, 2, 2
`+ itertools.cycle +`の例

_ _ + cycle(iterable)+

iterableから要素を使い果たすまで返します。 その後、シーケンスを無期限に繰り返します。 _ _

>>>

>>> cycle(['a', 'b', 'c'])
a, b, c, a, b, c, a, ...
`+ itertools累積+`の例

_ _ + accumulate(iterable、func = operator.add)+

累積和のシリーズ(または他のバイナリ関数の結果)を返します。 _ _

>>>

>>> accumulate([1, 2, 3])
1, 3, 6

じゃあ、数学を休んで、カードを使って楽しみましょう。

カードのデッキを扱う

ポーカーアプリを構築しているとします。 カードのデッキが必要です。 ランクのリスト(エース、キング、クイーン、ジャック、10、9など)とスーツのリスト(ハート、ダイヤモンド、クラブ、スペード)を定義することから始めます。

ranks = ['A', 'K', 'Q', 'J', '10', '9', '8', '7', '6', '5', '4', '3', '2']
suits = ['H', 'D', 'C', 'S']

カードを、最初の要素がランクで、2番目の要素がスーツであるタプルとして表すことができます。 カードのデッキは、そのようなタプルのコレクションになります。 デッキは本物のように振る舞う必要があるため、カードを1枚ずつ生成し、すべてのカードが配られると使い果たされるジェネレーターを定義することは理にかなっています。

これを実現する1つの方法は、 `+ ranks `と ` suits `のネストされた ` for +`ループを持つジェネレーターを書くことです。

def cards():
    """Return a generator that yields playing cards."""
    for rank in ranks:
        for suit in suits:
            yield rank, suit

ジェネレーター式を使用すると、これをよりコンパクトに記述できます。

cards = ((rank, suit) for rank in ranks for suit in suits)

ただし、これは、明示的にネストされた「+ for +」ループよりも実際に理解するのが難しいと主張する人もいるかもしれません。

入れ子になった `+ for +`ループを数学的な観点から、つまり、2つ以上の反復可能要素のhttps://en.wikipedia.org/wiki/Cartesian_product[Cartesian product]として表示するのに役立ちます。 数学では、2つのセット_A_と_B_のデカルト積は、_(a、b)_形式のすべてのタプルのセットです。ここで、_a_は_A_の要素であり、_b_は_B_の要素です。

Pythonイテラブルの例を次に示します。+ A = [1、2] + `と + B = ['a'、 'b'] + のデカルト積は + [(1、 'a')、 (1、 'b')、(2、 'a')、(2、 'b')] + `。

`+ itertools.product()+`関数はまさにこの状況のた​​めのものです。 任意の数の反復可能要素を引数として受け取り、デカルト積のタプルの反復子を返します。

it.product([1, 2], ['a', 'b'])  # (1, 'a'), (1, 'b'), (2, 'a'), (2, 'b')

`+ product()`関数は、2つの反復可能オブジェクトに決して限定されません。 好きなだけ渡すことができます。すべて同じサイズである必要はありません。 ` product([1、2、3]、['a'、 'b']、['c'])+`が何であるかを予測できるかどうかを確認し、インタープリターで実行して作業を確認します。

_ 警告: `+ product()+`関数は別の「ブルートフォース」関数であり、注意しないと組み合わせ爆発を引き起こす可能性があります。 _

`+ product()`を使用すると、 ` cards +`を1行で書き直すことができます。

cards = it.product(ranks, suits)

これはすべてうまくいきますが、その価値のあるポーカーアプリは、シャッフルデッキから始める方がよいでしょう。

import random

def shuffle(deck):
    """Return iterator over shuffled deck."""
    deck = list(deck)
    random.shuffle(deck)
    return iter(tuple(deck))

cards = shuffle(cards)

_ _ *注意: *`+ random.shuffle()`関数はhttps://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle[Fisher-Yates shuffle]を使用してリスト(または任意の可変)をシャッフルしますシーケンス)インプレースhttps://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#The_modern_algorithm[in _O(n)_ time]。 このアルゴリズムは、バイアスのない順列を生成するため、「 cards 」をシャッフルするのに適しています。つまり、反復可能オブジェクトのすべての順列は、「 random.shuffle()+」によって等しく返される可能性が高くなります。

とは言っても、 `+ shuffle()`は、 ` list(deck)`を呼び出すことにより、入力 ` deck +`のコピーをメモリ内に作成することに気づいたでしょう。 これはこの記事の精神に反するように見えますが、この著者はコピーを作成せずにイテレーターをシャッフルする良い方法を知りません。 _ _

ユーザーへの礼儀として、あなたは彼らにデッキを切る機会を与えたいと思います。 カードがテーブルにきちんと積み重ねられていると想像するなら、ユーザーに番号を選んで_n_、スタックの一番上から最初の_n_カードを取り除いて下に移動させます。

slicingについて1つまたは2つのことを知っている場合、次のように実行できます。

def cut(deck, n):
    """Return an iterator over a deck of cards cut at index `n`."""
    if n < 0:
        raise ValueError('`n` must be a non-negative integer')

    deck = list(deck)
    return iter(deck[n:] + deck[:n])

cards = cut(cards, 26)  # Cut the deck in half.

`+ cut()`関数は最初に ` deck `をリストに変換して、スライスしてカットできるようにします。 スライスが期待どおりに動作することを保証するには、「 n +」が負でないことを確認する必要があります。 そうでない場合は、例外をスローして、クレイジーなことが起こらないようにすることをお勧めします。

デッキのカットは非常に簡単です。カットされたデッキの上部は + deck [:n] +`で、下部は残りのカード、つまり `+ deck [n:] +`です。 上部の「半分」を下部に移動して新しいデッキを構築するには、それを下部に追加するだけです: `+ deck [n:] + deck [:n] +

+ cut()+`関数は非常に単純ですが、いくつかの問題があります。 リストをスライスすると、元のリストのコピーが作成され、選択した要素を含む新しいリストが返されます。 わずか52枚のカードのデッキでは、このスペースの複雑さの増加はささいなことですが、 `+ itertools +`を使用してメモリのオーバーヘッドを削減できます。 これを行うには、3つの関数、 `+ itertools.tee()++ itertools.islice()+、および `+ itertools.chain()+`が必要です。

これらの機能がどのように機能するかを見てみましょう。

+ tee()+`関数を使用して、単一の反復可能オブジェクトから任意の数の独立した反復子を作成できます。 引数は2つあります。1つ目は反復可能な `+ inputs +、2つ目は `+ inputs `を返す独立したイテレータの数 ` n `です(デフォルトでは、 ` n `は2に設定されています)。 イテレータは長さ ` n +`のタプルで返されます。

>>>

>>> iterator1, iterator2 = it.tee([1, 2, 3, 4, 5], 2)
>>> list(iterator1)
[1, 2, 3, 4, 5]
>>> list(iterator1)  # iterator1 is now exhausted.
[]
>>> list(iterator2)  # iterator2 works independently of iterator1
[1, 2, 3, 4, 5].

`+ tee()`は独立したイテレータを作成するのに便利ですが、内部でどのように機能するかについて少し理解することが重要です。 独立したイテレータを作成するために ` tee()+`を呼び出すとき、各イテレータは本質的に独自のFIFOキューで動作します。

1つの反復子から値が抽出されると、その値は他の反復子のキューに追加されます。 したがって、1つのイテレータが他のイテレータより先に使い果たされると、残りの各イテレータはメモリ内のイテレータ全体のコピーを保持します。 ( + itertools + docsで `+ tee()+`をエミュレートするPython関数を見つけることができます。)

このため、 `+ tee()`は注意して使用する必要があります。 ` tee()`によって返される他のイテレータで作業する前にイテレータの大部分を使い果たしている場合、入力イテレータを ` list `または ` tuple +`にキャストした方が良いかもしれません。

`+ islice()`関数は、リストまたはタプルをスライスするのとほぼ同じように機能します。 反復可能な開始点と停止点を渡し、リストをスライスするように、返されるスライスは停止点の直前のインデックスで停止します。 オプションで、ステップ値も含めることができます。 ここでの最大の違いは、もちろん、 ` islice()+`がイテレータを返すことです。

>>>

>>> # Slice from index 2 to 4
>>> list(it.islice('ABCDEFG', 2, 5)))
['C' 'D' 'E']

>>> # Slice from beginning to index 4, in steps of 2
>>> list(it.islice([1, 2, 3, 4, 5], 0, 5, 2))
[1, 3, 5]

>>> # Slice from index 3 to the end
>>> list(it.islice(range(10), 3, None))
[3, 4, 5, 6, 7, 8, 9]

>>> # Slice from beginning to index 3
>>> list(it.islice('ABCDE', 4))
['A', 'B', 'C', 'D']

上記の最後の2つの例は、イテラブルを切り捨てるのに役立ちます。 これを使用して、 `+ cut()`で使用されているリストのスライスを置き換えて、デッキの「上部」と「下部」を選択できます。 追加のボーナスとして、「 islice()」は開始/停止位置とステップ値の負のインデックスを受け入れないため、「 n +」が負の場合に例外を発生させる必要はありません。

最後に必要な関数は `+ chain()+`です。 この関数は、引数として任意の数の反復可能要素を取り、それらを一緒に「連鎖」します。 例えば:

>>>

>>> list(it.chain('ABC', 'DEF'))
['A' 'B' 'C' 'D' 'E' 'F']

>>> list(it.chain([1, 2], [3, 4, 5, 6], [7, 8, 9]))
[1, 2, 3, 4, 5, 6, 7, 8, 9]

武器庫に追加の火力が追加されたので、 `+ cut()`関数を書き直して、メモリに完全なコピー ` cards +`を作成せずにカードのデッキをカットできます。

def cut(deck, n):
    """Return an iterator over a deck of cards cut at index `n`."""
    deck1, deck2 = it.tee(deck, 2)
    top = it.islice(deck1, n)
    bottom = it.islice(deck2, n, None)
    return it.chain(bottom, top)

cards = cut(cards, 26)

カードをシャッフルしてカットしたので、いくつかのハンドを処理する時間です。 デッキ、ハンドの数、ハンドのサイズを引数として取り、指定された数のハンドを含むタプルを返す関数 `+ deal()+`を書くことができます。

この関数を書くのに新しい `+ itertools +`関数は必要ありません。 先に読む前に、自分で思いつくものを見てください。

1つの解決策は次のとおりです。

def deal(deck, num_hands=1, hand_size=5):
    iters = [iter(deck)]* hand_size
    return tuple(zip(*(tuple(it.islice(itr, num_hands)) for itr in iters)))

まず、 `+ deck `のイテレータへの ` hand_size `参照のリストを作成します。 次に、このステップを繰り返して、各ステップで「 num_hands +」カードを削除し、タプルに保存します。

次に、これらのタプルを「+ zip()」して、各プレーヤーに一度に1枚ずつカードを配ることをエミュレートします。 これにより、それぞれ「 hand_size 」カードを含む「 num_hands +」タプルが生成されます。 最後に、手を一度にタプルにパッケージ化して返します。

この実装は、「+ num_hands 」のデフォルト値を「+1」に、「+ hand_size 」を「+5」に設定します。「5枚のカードを引く」アプリを作成している可能性があります。 以下に、この関数の使用方法とサンプル出力を示します。

>>>

>>> p1_hand, p2_hand, p3_hand = deal(cards, num_hands=3)
>>> p1_hand
(('A', 'S'), ('5', 'S'), ('7', 'H'), ('9', 'H'), ('5', 'H'))
>>> p2_hand
(('10', 'H'), ('2', 'D'), ('2', 'S'), ('J', 'C'), ('9', 'C'))
>>> p3_hand
(('2', 'C'), ('Q', 'S'), ('6', 'C'), ('Q', 'H'), ('A', 'C'))

「+ cards +」の状態は、5枚のカードのうち3枚のハンドを配ったということだと思いますか?

>>>

>>> len(tuple(cards))
37

配られた15枚のカードは `+ cards `イテレータから消費されます。これはまさにあなたが望むものです。 そのようにして、ゲームが続行されると、「 cards +」イテレータの状態はプレイ中のデッキの状態を反映します。

セクションの要約

このセクションで見た `+ itertools +`関数を見てみましょう。

`+ itertools.product +`の例

_ _ + product(* iterables、repeat = 1)+

入力イテラブルのデカルト積。 ネストされたforループと同等です。 _ _

>>>

>>> product([1, 2], ['a', 'b'])
(1, 'a'), (1, 'b'), (2, 'a'), (2, 'b')
`+ itertools.tee +`の例

_ _ + tee(反復可能、n = 2)+

単一の入力反復可能オブジェクトから任意の数の独立した反復子を作成します。 _ _

>>>

>>> iter1, iter2 = it.tee(['a', 'b', 'c'], 2)
>>> list(iter1)
['a', 'b', 'c']
>>> list(iter2)
['a', 'b', 'c']
`+ itertools.islice +`の例

_ _ + islice(iterable、stop)+ + islice(iterable、start、stop、step = 1)+

`+ next ()`メソッドがイテラブルから選択された値を返すイテレータを返します。 リストの ` slice()+`のように機能しますが、イテレータを返します。 _ _

>>>

>>> islice([1, 2, 3, 4], 3)
1, 2, 3

>>> islice([1, 2, 3, 4], 1, 2)
2, 3
`+ itertools.chain +`の例

_ _ + chain(* iterables)+

`+ next ()+`メソッドが最初の反復可能オブジェクトから要素を使い果たすまでチェーン要素を返し、次に次の反復可能要素から要素をすべての反復可能要素が使い果たされるまで返します。 _ _

>>>

>>> chain('abc', [1, 2, 3])
'a', 'b', 'c', 1, 2, 3

休憩:リストのフラット化

前の例では、 `+ chain()`を使用して、1つのイテレーターを別のイテレーターの最後に追加しました。 ` chain()`関数には、単一のイテラブルを引数として取るクラスメソッド ` .from_iterable()`があります。 iterableの要素自体が反復可能でなければならないため、最終的な効果は ` chain.from_iterable()+`が引数を平坦化することです:

>>>

>>> list(it.chain.from_iterable([[There’s no reason the argument of `+chain.from_iterable()+` needs to be finite. You could emulate the behavior of `+cycle()+`, for example:

[source,python]

>>> cycle = it.chain.from_iterable(it.repeat( 'abc'))>>> list(it.islice(cycle、8))

The `+chain.from_iterable()+` function is useful when you need to build an iterator over data that has been “chunked.”

In the next section, you will see how to use `+itertools+` to do some data analysis on a large dataset. But you deserve a break for having stuck with it this far. Why not hydrate yourself and relax a bit? Maybe even play a little https://archive.org/details/Trek-nTheNthIteration[Star Trek: The Nth Iteration].

Back? Great! Let’s do some data analysis.

=== Analyzing the S&P500

In this example, you will get your first taste of using `+itertools+` to manipulate a large dataset—in particular, the historical daily price data of the S&P500 index. A CSV file `+SP500.csv+` with this data can be found https://github.com/realpython/materials/tree/master/itertools-in-python3[here] (source: https://finance.yahoo.com/quote/%5EGSPC?p=%5EGSPC[Yahoo Finance]). The problem you’ll tackle is this:

____
Determine the maximum daily gain, daily loss (in percent change), and the longest growth streak in the history of the S&P500.
____

To get a feel for what you’re dealing with, here are the first ten rows of `+SP500.csv+`:

[source,sh]

$ head -n 10 SP500.csv Date、Open、High、Low、Close、Adj Close、Volume 1950-01-03,16.660000,16.660000,16.660000,16.660000,16.660000,1260000 1950-01-04,16.850000,16.850000,16.850000 、16.850000、16.850000、1890000 1950-01-05、16.930000、16.930000、16.930000、16.930000、16.930000、2550000 1950-01-06、16.980000、16.980000、16.980000、16.980000、16.980000、2010000 1950-01-09、17.080000、17.080000、 17.080000,17.080000,17.080000,2520000 1950-01-10,17.030001,17.030001,17.030001,17.030001,17.030001,2160000 1950-01-11,17.090000,17.090000,17.090000,17.090000,17.090000,2630000 1950-01-12,16.760000,16.760000 、16.760000、16.760000、16.760000、2970000 1950-01-13、16.670000、16.670000、16.670000、16.670000、16.670000、3330000

As you can see, the early data is limited. The data improves for later dates, and, as a whole, is sufficient for this example.

The strategy for solving this problem is as follows:

* Read data from the CSV file and transform it into a sequence `+gains+` of daily percent changes using the “Adj Close” column.
* Find the maximum and minimum values of the `+gains+` sequence, and the date on which they occur. (Note that it is possible that these values are attained on more than on date; in that case, the most recent date will suffice.)
* Transform `+gains+` into a sequence `+growth_streaks+` of tuples of consecutive positive values in `+gains+`. Then determine the length of the longest tuple in `+growth_streaks+` and the beginning and ending dates of the streak. (It is possible that the maximum length is attained by more than one tuple in `+growth_streaks+`; in that case, the tuple with the most recent beginning and ending dates will suffice.)

The _percent change_ between two values _x_ and _y_ is given by the following formula:

https://files.realpython.com/media/percent_change.3c81a67b1906.png[image:https://files.realpython.com/media/percent_change.3c81a67b1906.png[Percent Change Formula,width=1097,height=200]]

For each step in the analysis, it is necessary to compare values associated with dates. To facilitate these comparisons, you can subclass the https://docs.python.org/3/library/collections.html#collections.namedtuple[`+namedtuple+` object] from the https://docs.python.org/3/library/collections.html[`+collections+` module]:

[source,python]

コレクションからインポートnamedtuple

クラスDataPoint(namedtuple( 'DataPoint'、['date'、 'value'])):slots =()

def le (self、other):self.value ⇐ other.valueを返します

def lt (self、other):self.value <other.valueを返します

def gt (self、other):self.value> other.valueを返します

The `+DataPoint+` class has two attributes: `+date+` (a https://docs.python.org/3/library/datetime.html#datetime.datetime[`+datetime.datetime+`] instance) and `+value+`. The https://docs.python.org/3/reference/datamodel.html#object.__le__[`+.__le__()+`], https://docs.python.org/3/reference/datamodel.html#object.__lt__[`+.__lt__()+`] and https://docs.python.org/3/reference/datamodel.html#object.__gt__[`+.__gt__()+`] https://realpython.com/operator-function-overloading/[dunder methods] are implemented so that the `+<=+`, `+<+`, and `+>+` boolean comparators can be used to compare the values of two `+DataPoint+` objects. This also allows the https://docs.python.org/3/library/functions.html#max[`+max()+`] and https://docs.python.org/3/library/functions.html#min[`+min()+`] built-in functions to be called with `+DataPoint+` arguments.

____
*Note: *If you are not familiar with https://docs.python.org/3/library/collections.html#collections.namedtuple[`+namedtuple+`], check out https://dbader.org/blog/writing-clean-python-with-namedtuples[this excellent resource]. The `+namedtuple+` implementation for `+DataPoint+` is just one of many ways to build this data structure. For example, in Python 3.7 you could implement `+DataPoint+` as a data class. Check out our https://realpython.com/python-data-classes/[Ultimate Guide to Data Classes] for more information.
____

The following reads the data from `+SP500.csv+` to a tuple of `+DataPoint+` objects:

[source,python]

日時からCSVをインポートします。

def read_prices(csvfile、_strptime = datetime.strptime):infileとしてopen(csvfile)を使用:reader = csv.DictReader(infile)in row in reader:yield DataPoint(date = _strptime(row ['Date']、 '%Y -%m-%d ')。date()、value = float(row [' Adj Close ']))

価格= tuple(read_prices( 'SP500.csv'))

The `+read_prices()+` generator opens `+SP500.csv+` and reads each row with a https://docs.python.org/3/library/csv.html#csv.DictWriter[`+csv.DictReader()+`] object. `+DictReader()+` returns each row as an https://docs.python.org/3/library/collections.html#collections.OrderedDict[`+OrderedDict+`] whose keys are the column names from the header row of the CSV file.

For each row, `+read_prices()+` yields a `+DataPoint+` object containing the values in the “Date” and “Adj Close” columns. Finally, the full sequence of data points is committed to memory as a `+tuple+` and stored in the `+prices+` variable.

Next, `+prices+` needs to be transformed to a sequence of daily percent changes:

[source,python]

gains = tuple(DataPoint(day.date、100* (day.value/prev_day.value-1.))day、prev_day in zip(prices [1:]、price))

The choice of storing the data in a `+tuple+` is intentional. Although you could point `+gains+` to an iterator, you will need to iterate over the data twice to find the minimum and maximum values.

If you use `+tee()+` to create two independent iterators, exhausting one iterator to find the maximum will create a copy of all of the data in memory for the second iterator. By creating a `+tuple+` up front, you do not lose anything in terms of space complexity compared to `+tee()+`, and you may even gain a little speed.

____
*Note: *This example focuses on leveraging `+itertools+` for analyzing the S&P500 data. Those intent on working with a lot of time series financial data might also want to check out the https://pandas.pydata.org/[Pandas] library, which is well suited for such tasks.
____

==== Maximum Gain and Loss

To determine the maximum gain on any single day, you might do something like this:

[source,python]

ゲインのdata_pointのmax_gain = DataPoint(None、0):max_gain = max(data_point、max_gain)

print(max_gain)#DataPoint(date = '2008-10-28'、value = 11.58)

You can simplify the `+for+` loop using the https://docs.python.org/3/library/functools.html#functools.reduce[`+functools.reduce()+` function]. This function accepts a binary function `+func+` and an iterable `+inputs+` as arguments, and “reduces” `+inputs+` to a single value by applying `+func+` cumulatively to pairs of objects in the iterable.

For example, `+functools.reduce(operator.add, [1, 2, 3, 4, 5])+` will return the sum `+1 + 2 + 3 + 4 + 5 = 15+`. You can think of `+reduce()+` as working in much the same way as `+accumulate()+`, except that it returns only the final value in the new sequence.

Using `+reduce()+`, you can get rid of the `+for+` loop altogether in the above example:

[source,python]

functoolsをftとしてインポート

max_gain = ft.reduce(max、gains)

print(max_gain)#DataPoint(date = '2008-10-28'、value = 11.58)

The above solution works, but it isn’t equivalent to the `+for+` loop you had before. Do you see why? Suppose the data in your CSV file recorded a loss every single day. What would the value of `+max_gain+` be?

In the `+for+` loop, you first set `+max_gain = DataPoint(None, 0)+`, so if there are no gains, the final `+max_gain+` value will be this empty `+DataPoint+` object. However, the `+reduce()+` solution returns the smallest loss. That is not what you want and could introduce a difficult to find bug.

This is where `+itertools+` can help you out. The `+itertools.filterfalse()+` function takes two arguments: a function that returns `+True+` or `+False+` (called a* predicate*), and an iterable `+inputs+`. It returns an iterator over the elements in `+inputs+` for which the predicate returns `+False+`.

Here’s a simple example:

[source,python]

>>> only_positives = it.filterfalse(lambda x:x ⇐ 0、[0、1、-1、2、-2])>>> list(only_positives)

You can use `+filterfalse()+` to filter out the values in `+gains+` that are negative or zero so that `+reduce()+` only works on positive values:

[source,python]

max_gain = ft.reduce(max、it.filterfalse(lambda p:p ⇐ 0、gains))

What happens if there are never any gains? Consider the following:

[source,python]

>>> ft.reduce(max、it.filterfalse(lambda x:x ⇐ 0、[-1、-2、-3])))トレースバック(最後の最後の呼び出し):ファイル "<stdin>"、1行目、<モジュール> TypeErrorで:初期値のない空のシーケンスのreduce()

Well, that’s not what you want! But, it makes sense because the iterator returned by `+filterflase()+` is empty. You could handle the `+TypeError+` by wrapping the call to `+reduce()+` with `+try...except+`, but there’s a better way.

The `+reduce()+` function accepts an optional third argument for an initial value. Passing `+0+` to this third argument gets you the expected behavior:

[.repl-toggle]#>>>#

[source,python,repl]

>>> ft.reduce(max、it.filterfalse(lambda x:x ⇐ 0、[-1、-2、-3])、0)0

Applying this to the S&P500 example:

[source,python]

zdp = DataPoint(None、0)#zero DataPoint max_gain = ft.reduce(max、it.filterfalse(lambda p:p ⇐ 0、diffs)、zdp)

Great! You’ve got it working just the way it should! Now, finding the maximum loss is easy:

[source,python]

max_loss = ft.reduce(min、it.filterfalse(lambda p:p> 0、gains)、zdp)

print(max_loss)#DataPoint(date = '2018-02-08'、value = -20.47)

==== Longest Growth Streak

Finding the longest growth streak in the history of the S&P500 is equivalent to finding the largest number of consecutive positive data points in the `+gains+` sequence. The `+itertools.takewhile()+` and `+itertools.dropwhile()+` functions are perfect for this situation.

The `+takewhile()+` function takes a predicate and an iterable `+inputs+` as arguments and returns an iterator over `+inputs+` that stops at the first instance of an element for which the predicate returns `+False+`:

[source,python]

it.takewhile(lambda x:x <3、[0、1、2、3、4])#0、1、2

The `+dropwhile()+` function does exactly the opposite. It returns an iterator beginning at the first element for which the predicate returns `+False+`:

[source,python]

it.dropwhile(lambda x:x <3、[0、1、2、3、4])#3、4

In the following generator function, `+takewhile()+` and `+dropwhile()+` are composed to yield tuples of consecutive positive elements of a sequence:

[source,python]

def Continuous_positives(sequence、zero = 0):def _consecutives():itのit.repeat(iter(sequence)):yield tuple(it.takewhile(lambda p:p> 0、it.dropwhile(lambda p:p ⇐ゼロ、itr)))it.takewhile(lambda t:len(t)、_consecutives())を返します

The `+consecutive_positives()+` function works because `+repeat()+` keeps returning a pointer to an iterator over the `+sequence+` argument, which is being partially consumed at each iteration by the call to `+tuple()+` in the `+yield+` statement.

You can use `+consecutive_positives()+` to get a generator that produces tuples of consecutive positive data points in `+gains+`:

[source,python]

growth_streaks = Continuous_positives(gains、zero = DataPoint(None、0))

Now you can use `+reduce()+` to extract the longest growth streak:

[source,python]

longest_streak = ft.reduce(lambda x、y:x if len(x)> len(y)else y、growth_streaks)

Putting the whole thing together, here’s a full script that will read data from the `+SP500.csv+` file and print out the max gain/loss and longest growth streak:

[source,python]

コレクションからnamedtupleをインポートdatetimeからcsvをインポートdatetimeからitctoolsをインポートします

クラスDataPoint(namedtuple( 'DataPoint'、['date'、 'value'])):slots =()

def le (self、other):self.value ⇐ other.valueを返します

def lt (self、other):self.value <other.valueを返します

def gt (self、other):self.value> other.valueを返します

def Continuous_positives(sequence、zero = 0):def _consecutives():itのit.repeat(iter(sequence)):yield tuple(it.takewhile(lambda p:p> 0、it.dropwhile(lambda p:p ⇐ゼロ、itr)))it.takewhile(lambda t:len(t)、_consecutives())を返します

def read_prices(csvfile、_strptime = datetime.strptime):infileとしてopen(csvfile)を使用:reader = csv.DictReader(infile)in row in reader:yield DataPoint(date = _strptime(row ['Date']、 '%Y -%m-%d ')。date()、value = float(row [' Adj Close ']))

#価格を読み取り、毎日の変化率を計算します。 価格= tuple(read_prices( 'SP500.csv'))ゲイン= tuple(DataPoint(day.date、100 *(day.value/prev_day.value-1.))の場合、zip(prices [1:]のprev_day 、価格))

#毎日の最大の利益/損失を見つけます。 zdp = DataPoint(None、0)#zero DataPoint max_gain = ft.reduce(max、it.filterfalse(lambda p:p ⇐ zdp、gains))max_loss = ft.reduce(min、it.filterfalse(lambda p:p > zdp、ゲイン)、zdp)

#最長の成長線を見つけます。 growth_streaks = Continuous_positives(gains、zero = DataPoint(None、0))longest_streak = ft.reduce(lambda x、y:x if len(x)> len(y)else y、growth_streaks)

#結果を表示します。 print( 'Max gain:{1:} 2f}%on {0}'。format(* max_gain))print( 'Max loss:{1:.2f}%on {0}'。format(* max_loss))

print( 'Longest growth streak:{num_days} days({first} to {last})'。format(num_days = len(longest_streak)、first = longest_streak [0] .date、last = longest_streak [-1] .date) )

Running the above script produces the following output:

[source,sh]

最大ゲイン:2008-10-13で11.58%最大損失:1987-10-19で-20.47%最長成長ストリーク:14日間(1971-03-26から1971-04-15)

[[section-recap_3]]
==== Section Recap

In this section, you covered a lot of ground, but you only saw a few functions from `+itertools+`. Let’s review those now.

[[itertoolsfilterfalse-example]]
===== `+itertools.filterfalse+` Example

____
`+filterfalse(pred, iterable)+`

Return those items of sequence for which `+pred(item)+` is false. If `+pred+` is `+None+`, return the items that are false.
____

[.repl-toggle]#>>>#

[source,python,repl]

>>> filterfalse(bool、[1、0、1、0、0])0、0、0

[[itertoolstakewhile-example]]
===== `+itertools.takewhile+` Example

____
`+takewhile(pred, iterable)+`

Return successive entries from an iterable as long as `+pred+` evaluates to true for each entry.
____

[.repl-toggle]#>>>#

[source,python,repl]

>>> takewhile(bool、[1、1、1、0、0])1、1、1

[[itertoolsdropwhile-example]]
===== `+itertools.dropwhile+` Example

____
`+dropwhile(pred, iterable)+`

Drop items from the iterable while `+pred(item)+` is true. Afterwards, return every element until the iterable is exhausted.
____

[.repl-toggle]#>>>#

[source,python,repl]

>>> dropwhile(bool、[1、1、1、0、0、1、1、0])0、0、1、1、0

You are really starting to master this whole `+itertools+` thing! The community swim team would like to commission you for a small project.

=== Building Relay Teams From Swimmer Data

In this example, you will read data from a CSV file containing swimming event times for a community swim team from all of the swim meets over the course of a season. The goal is to determine which swimmers should be in the relay teams for each stroke next season.

Each stroke should have an “A” and a “B” relay team with four swimmers each. The “A” team should contain the four swimmers with the best times for the stroke and the “B” team the swimmers with the next four best times.

The data for this example can be found https://github.com/realpython/materials/tree/master/itertools-in-python3[here]. If you want to follow along, download it to your current working directory and save it as `+swimmers.csv+`.

Here are the first 10 rows of `+swimmers.csv+`:

[source,sh]

$ head -n 10 swimmers.csv Event、Name、Stroke、Time1、Time2、Time3 0、Emma、freestyle、00:50:313667,00:50:875398,00:50:646837 0、Emma、backstroke、00: 56:720191,00:56:431243,00:56:941068 0、Emma、butterfly、00:41:927947,00:42:062812,00:42:007531 0、Emma、breaststroke、00:59:825463、 00:59:397469,00:59:385919 0、Olivia、freestyle、00:45:566228,00:46:066985,00:46:044389 0、Olivia、backstroke、00:53:984872,00:54: 575110,00:54:932723 0、Olivia、butterfly、01:12:548582,01:12:722369,01:13:105429 0、Olivia、breaststroke、00:49:230921,00:49:604561,00: 49:120964 0、Sophia、freestyle、00:55:209625,00:54:790225,00:55:351528

The three times in each row represent the times recorded by three different stopwatches, and are given in `+MM:SS:mmmmmm+` format (minutes, seconds, microseconds). The accepted time for an event is the _median_ of these three times, _not_ the average.

Let’s start by creating a subclass `+Event+` of the `+namedtuple+` object, just like we did in the link:#analyzing-the-sp500[SP500 example]:

[source,python]

コレクションからインポートnamedtuple

class Event(namedtuple( 'Event'、['stroke'、 'name'、 'time'])):slots =()

def lt (self、other):self.time <other.timeを返します

The `+.stroke+` property stores the name of the stroke in the event, `+.name+` stores the swimmer name, and `+.time+` records the accepted time for the event. The `+.__lt__()+` dunder method will allow `+min()+` to be called on a sequence of `+Event+` objects.

To read the data from the CSV into a tuple of `+Event+` objects, you can use the `+csv.DictReader+` object:

[source,python]

import csv import datetime import statistics

def read_events(csvfile、strptime = datetime.datetime.strptime):def _median(times):return statistics.median(( strptime(time、 '%M:%S:%f')。time()in time in row [ 'Times'])))

fieldnames = ['Event'、 'Name'、 'Stroke'] with open(csvfile)as infile:reader = csv.DictReader(infile、fieldnames = fieldnames、restkey = 'Times')next(reader)#行のヘッダーをスキップリーダー:yield Event(row ['Stroke']、row ['Name']、_median(row ['Times']))

イベント= tuple(read_events( 'swimmers.csv'))

The `+read_events()+` generator reads each row in the `+swimmers.csv+` file into an `+OrderedDict+` object in the following line:

[source,python]

reader = csv.DictReader(infile、fieldnames = fieldnames、restkey = 'Times')

By assigning the `+'Times'+` field to `+restkey+`, the “Time1”, “Time2”, and “Time3” columns of each row in the CSV file will be stored in a list on the `+'Times'+` key of the `+OrderedDict+` returned by `+csv.DictReader+`.

For example, the first row of the file (excluding the header row) is read into the following object:

[source,python]

OrderedDict([( 'Event'、 '0')、( 'Name'、 'Emma')、( 'Stroke'、 'freestyle')、( 'Times'、['00:50:313667 '、'00: 50:875398 '、' 00:50:646837 '])])

Next, `+read_events()+` yields an `+Event+` object with the stroke, swimmer name, and median time (as a https://docs.python.org/3/library/datetime.html#time-objects[`+datetime.time+` object]) returned by the `+_median()+` function, which calls https://docs.python.org/3/library/statistics.html#statistics.median[`+statistics.median()+`] on the list of times in the row.

Since each item in the list of times is read as a string by `+csv.DictReader()+`, `+_median()+` uses the https://docs.python.org/3/library/datetime.html#datetime.datetime.strptime[`+datetime.datetime.strptime()+` classmethod] to instantiate a time object from each string.

Finally, a tuple of `+Event+` objects is created:

[source,python]

イベント= tuple(read_events( 'swimmers.csv'))

The first five elements of `+events+` look like this:

[.repl-toggle]#>>>#

[source,python,repl]

>>> events [:5](Event(stroke = 'freestyle'、name = 'Emma'、time = datetime.time(0、0、50、646837)))、Event(stroke = 'backstroke'、name = ' Emma '、time = datetime.time(0、0、56、720191))、Event(stroke =' butterfly '、name =' Emma '、time = datetime.time(0、0、42、7531))、イベント(stroke = 'breaststroke'、name = 'Emma'、time = datetime.time(0、0、59、397469))、Event(stroke = 'freestyle'、name = 'Olivia'、time = datetime.time(0 、0、46、44389)))

Now that you’ve got the data into memory, what do you do with it? Here’s the plan of attack:

* Group the events by stroke.
* For each stroke:
** Group its events by swimmer name and determine the best time for each swimmer.
** Order the swimmers by best time.
* *The first four swimmers make the “A” team for the stroke, and the next four swimmers make the “B” team.

The `+itertools.groupby()+` function makes grouping objects in an iterable a snap. It takes an iterable `+inputs+` and a `+key+` to group by, and returns an object containing iterators over the elements of `+inputs+` grouped by the key.

Here’s a simple `+groupby()+` example:

[.repl-toggle]#>>>#

[source,python,repl]

>>> data = [{'name': 'Alan'、 'age':34}、 . {「名前」:「キャサリン」、「年齢」:34}、 . {「名前」:「ベッツィー」、「年齢」:29}、 . {'name': 'David'、 'age':33}] …​ >>> grouped_data = it.groupby(data、key = lambda x:x ['age'])>>>キーの場合、grouped_dataのgrp: . print( '{}:{}'。format(key、list(grp))) …​ 34:[{'name': 'Alan'、 'age':34}、{'name': 'Betsy'、 'age':34}] 29:[{'name': 'Catherine'、 'age' :29}] 33:[{'name': 'David'、 'age':33}]

If no key is specified, `+groupby()+` defaults to grouping by “identity”—that is, aggregating identical elements in the iterable:

[.repl-toggle]#>>>#

[source,python,repl]

>>>キーについては、grp in it.groupby([1、1、2、2、2、3]: . print( '{}:{}'。format(key、list(grp))) …​ 1:[1、1] 2:[2、2、2] 3:[3]

The object returned by `+groupby()+` is sort of like a dictionary in the sense that the iterators returned are associated with a key. However, unlike a dictionary, it won’t allow you to access its values by key name:

[source,python]

>>> grouped_data [1]トレースバック(最後の最後の呼び出し):ファイル「<stdin>」、行1、<module> TypeError: 'itertools.groupby’オブジェクトは添字付け不可

In fact,* `+groupby()+` returns an iterator over tuples whose first components are keys and second components are iterators over the grouped data*:

[.repl-toggle]#>>>#

[source,python,repl]

>>> grouped_data = it.groupby([1、1、2、2、2、3])>>> list(grouped_data)[(1、<itertools._grouper object at 0x7ff3056130b8>)、(2、<itertools。 _grouperオブジェクトat 0x7ff3056130f0>)、(3、<itertools._grouper object at 0x7ff305613128>)]

One thing to keep in mind with `+groupby()+` is that it isn’t as smart as you might like. As `+groupby()+` traverses the data, it aggregates elements until an element with a different key is encountered, at which point it starts a new group:

[.repl-toggle]#>>>#

[source,python,repl]

>>> grouped_data = it.groupby([1、2、1、2、3、2])>>>キーの場合、grouped_dataのgrp: . print( '{}:{}'。format(key、list(grp))) …​ 1:[1] 2:[2] 1:[1] 2:[2] 3:[3] 2:[2]

Compare this to, say, the SQL `+GROUP BY+` command, which groups elements regardless of their order of appearance.

When working with `+groupby()+`, you need to sort your data on the same key that you would like to group by. Otherwise, you may get unexpected results. This is so common that it helps to write a utility function to take care of this for you:

[source,python]

def sort_and_group(iterable、key = None): "" "グループが` key`で `iterable`をソートしました。" "" it.groupby(sorted(iterable、key = key)、key = key)

Returning to the swimmers example, the first thing you need to do is create a for loop that iterates over the data in the `+events+` tuple grouped by stroke:

[source,python]

ストロークの場合、sort_and_groupのevts(events、key = lambda evt:evt.stroke):

Next, you need to group the `+evts+` iterator by swimmer name inside of the above `+for+` loop:

[source,python]

events_by_name = sort_and_group(evts、key = lambda evt:evt.name)

To calculate the best time for each swimmer in `+events_by_name+`, you can call `+min()+` on the events in that swimmers group. (This works because you implemented the `+.__lt__()+` dunder method in the `+Events+` class.)

[source,python]

best_times =(_のmin(evt)、events_by_nameのevt)

Note that the `+best_times+` generator yields `+Event+` objects containing the best stroke time for each swimmer. To build the relay teams, you’ll need to sort `+best_times+` by time and aggregate the result into groups of four. To aggregate the results, you can use the `+grouper()+` function from link:#the-grouper-recipe[The `+grouper()+` recipe] section and use `+islice()+` to grab the first two groups.

[source,python]

sort_by_time = sort(best_times、key = lambda evt:evt.time)teams = zip(( 'A'、 'B')、it.islice(grouper(sorted_by_time、4)、2))

Now `+teams+` is an iterator over exactly two tuples representing the “A” and the “B” team for the stroke. The first component of each tuple is the letter “A” or “B”, and the second component is an iterator over `+Event+` objects containing the swimmers in the team. You can now print the results:

[source,python]

チームの場合、チームのスイマー:print( '{stroke} {team}:{names}'。format(stroke = stroke.capitalize()、team = team、names = '、' .join(swimmer.name in swimmer inスイマー)))

Here’s the full script:

[source,python]

コレクションからnamedtupleをインポートcsvをインポートdatetimeをインポートitertools統計をインポートする

class Event(namedtuple( 'Event'、['stroke'、 'name'、 'time'])):slots =()

def lt (self、other):self.time <other.timeを返します

def sort_and_group(iterable、key = None):return it.groupby(sorted(iterable、key = key)、key = key)

def grouper(iterable、n、fillvalue = None):iters = [iter(iterable)] * n return it.zip_longest(* iters、fillvalue = fillvalue)

def read_events(csvfile、strptime = datetime.datetime.strptime):def _median(times):return statistics.median(( strptime(time、 '%M:%S:%f')。time()in time in row [ 'Times'])))

fieldnames = ['Event'、 'Name'、 'Stroke'] with open(csvfile)as infile:reader = csv.DictReader(infile、fieldnames = fieldnames、restkey = 'Times')next(reader)#ヘッダーをスキップします。 リーダーの行の場合:yield Event(row ['Stroke']、row ['Name']、_median(row ['Times']))

イベント= tuple(read_events( 'swimmers.csv'))

ストロークの場合、sort_and_groupのevts(events、key = lambda evt:evt.stroke):events_by_name = sort_and_group(evts、key = lambda evt:evt.name)best_times =(min(evt)for _、evt_events_by_name)sort_by_time = sort(best_times、key = lambda evt:evt.time)team = zip(( 'A'、 'B')、it.islice(grouper(sorted_by_time、4)、2))チーム、チームのスイマー:print( '{stroke} {team}:{names}'。format(stroke = stroke.capitalize()、team = team、names = '、' .join(swimmer.name in swimmer in swimmers)))

If you run the above code, you’ll get the following output:

[source,sh]

背泳ぎA:ソフィア、グレース、ペネロペ、アディソン背泳ぎB:エリザベス、オードリー、エミリー、アリアブレストストロークA:サマンサ、エイブリー、レイラ、ゾーイブレストストロークB:リリアン、アリア、アヴァ、アレクサバタフライA:オードリー、リア、レイラ、サマンサバタフライB:アレクサ、ゾーイ、エマ、マディソンフリースタイルA:オーブリー、エマ、オリビア、エブリンフリースタイルB:エリザベス、ゾーイ、アディソン、マディソン

=== Where to Go From Here

If you have made it this far, congratulations! I hope you have enjoyed the journey.

`+itertools+` is a powerful module in the Python standard library, and an essential tool to have in your toolkit. With it, you can write faster and more memory efficient code that is often simpler and easier to read (although that is not always the case, as you saw in the section on link:#second-order-recurrence-relations[second order recurrence relations]).

If anything, though, `+itertools+` is a testament to the power of iterators and https://en.wikipedia.org/wiki/Lazy_evaluation[lazy evaluation]. Even though you have seen many techniques, this article only scratches the surface.

So I guess this means your journey is only just beginning.

*Free Bonus:* https://realpython.com/bonus/itertools-cheatsheet/[Click here to get our itertools cheat sheet] that summarizes the techniques demonstrated in this tutorial.

In fact, this article skipped two `+itertools+` functions: https://docs.python.org/3/library/itertools.html#itertools.starmap[`+starmap()+`] and https://docs.python.org/3/library/itertools.html#itertools.compress[`+compress()+`]. In my experience, these are two of the lesser used `+itertools+` functions, but I urge you to read their docs an experiment with your own use cases!

Here are a few places where you can find more examples of `+itertools+` in action (thanks to Brad Solomon for these fine suggestions):

* https://stackoverflow.com/questions/9059173/what-is-the-purpose-in-pythons-itertools-repeat/9098860#9098860[What is the Purpose of `+itertools.repeat()+`]?
* https://stackoverflow.com/questions/48421142/fastest-way-to-generate-a-random-like-unique-string-with-random-length-in-python/48421303#48421303[Fastest Way to Generate a Random-like Unique String With Random Length in Python 3]
* https://stackoverflow.com/questions/49372880/write-pandas-dataframe-to-string-buffer-with-chunking/49374826#49374826[Write a Pandas DataFrame to a String Buffer with Chunking]

Finally, for even more tools for constructing iterators, take a look at https://github.com/erikrose/more-itertools[more-itertools].

Do you have any favorite `+itertools+` recipes/use-cases? We would love to hear about them in the comments!

_We would like to thank our readers Putcher and Samir Aghayev for pointing out a couple of errors in the original version of this article._