このガイドでは、Python の itertools
モジュールを使ってイテレータのパワーを利用する方法を見ていきます。
itertools` モジュールは、高速でメモリ効率の良いイテレータを作成するためのインターフェイスを提供します。
これらのイテレータは、無限、組合せ、または終端とすることができます。
イテレータとイテラブルの比較
Iterator は、Iterable (コンテナ) の項目を一定の順序で案内 (反復) することができる知的なポインターです。
色のリストと整数のリストを考えてみましょう。
colors = ['red', 'blue', 'pink']
ints = [1, 3, 5, 4, 2]
これらのリストを特定の順序で定義しても、メモリに配置するときに同じ順序で格納する必要はありません。
iterators: it1 it2
V V
memory: red 4 2 blue 1 3 pink 5
もし、メモリを順番通りに通過したら、 colors
配列の 2 番目の要素は 4
になってしまうでしょう。
イテレータの仕事は、メモリ上のどこにあっても、リストの次の要素を見つけることです。
これは、イテレータが指し示す次の要素を返す next()
メソッドによって行われます。
例えば、 it1
はアクセス可能なメモリを探し回って blue
を返し、 it2
は 3
を返します。
イテレータの大きな特徴は、それぞれのイテレータブルでどのように要素を検索するかを定義できることです。
例えば、すべての奇数をスキップして、そのサブセットを返すように要求することができます。
これは、カスタムの next()
メソッドを実装するか、ビルトインの itertools
を使用することで、様々な方法でオブジェクトを反復処理するための特定のイテレータを生成することで実現できます。
これから紹介するイテレーションツールは以下の通りです。
compress()
dropwhile()
takewhile()
groupby()
これらのイテレータ構築関数(イテレータを生成する関数)は、それぞれ単独で使うこともできますし、組み合わせて使うこともできます。
compress()関数
compress(data, selector)関数は、ブール値のリスト -
selectorに従って
dataから選択的に値を取り出すイテレータを作成する。
もし、dataの値が
selectorリスト中の
True` に一致する場合は、その値が選択され、一致しない場合はスキップされる。
dataと
selectorが同じサイズでない場合、
compress()は
dataまたは
selector` のリストのいずれかを使い切った時点で停止する。
# Importing the compress tool
from itertools import compress
cars = ['Audi', 'Volvo', 'Benz',
'BMW', 'Nissan', 'Mazda',
'Ford']
selector = [True, True, False, False,
False, True, False]
# This makes an iterator that filters elements,
# from data, for which selector values amount to True
my_cars = compress(cars, selector)
for each in my_cars:
print(each)
この結果
Audi
Volvo
Mazda
selectorには、
1と
0` のリストや、任意の真偽値を指定することができます。
通常、これらの真偽値のリストは、次のような何らかの条件によって取得されます。
int_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
boolean_list = [True if x % 2 == 0 else False for x in int_list]
# OR
boolean_list = [1 if x % 2 == 0 else 0 for x in int_list]
print(boolean_list)
ここでは、偶数ごとに True
を持つ boolean_list
を生成しています。
[False, True, False, True, False, True, False, True, False, True]
# OR
[0, 1, 0, 1, 0, 1, 0, 1, 0, 1]
一般的に、物事を短くするために、新しい変数に結果を代入せずに compress()
ツールや他のツールを使用することがあります。
import itertools
word = 'STACKABUSE'
selector = [1, 0, 1, 0, 0, 0, 0, 1, 1, 1]
for each in itertools.compress(word, selector ):
print(each)
その結果、次のようになります。
S
A
U
S
E
さらに技術的には、selector
の値を任意の真偽値で混在させることができます。
from itertools import compress
cars = ['Audi', 'Volvo', 'Benz',
'BMW', 'Nissan', 'Mazda', 'Ford']
# Empty string is falsy, non empty is truthy
selector = [True, 1, 0, 0, '', 1, 'string']
for each in compress(cars, selector):
print(each)
出力は次のようになります。
Audi
Volvo
Mazda
Ford
しかし、このようにリンゴとナシを混ぜることは推奨されないことに注意してください。
dropwhile()関数
dropwhile(criteria, sequence)関数は、
sequenceに含まれる全ての要素を削除する (読み飛ばす) イテレータを作成し、
criteria関数に渡すと
True` を返します。
この criteria
関数は通常ラムダ関数ですが、必ずしもラムダ関数である必要はありません。
通常、単純な関数であればラムダ関数に短縮されますが、複雑な関数の場合はそうではありません。
from itertools import dropwhile
int_list = [0, 1, 2, 3, 4, 5, 6]
result = list(dropwhile(lambda x : x < 3, int_list))
print(result)
このラムダ関数が与えられたとき、3
より小さい値を持つすべての要素は True
を返すので、3より小さい要素はすべてスキップされます。
criteriaがtrueの間、それらはドロップされます。
[3, 4, 5, 6]
ラムダ関数の代わりに、もっと複雑な関数を定義して、その関数の参照を渡すこともできます。
from itertools import dropwhile
def doesnt_contain_character(str):
substring = 'a'
if substring in str:
return False
else:
return True
string_list = ['lorem', 'ipsum', 'dolor', 'sit', 'amet']
print(list(dropwhile(doesnt_contain_character, string_list)))
例えば、このメソッドは文字列に substring
(この場合は a
) が含まれないかどうかをチェックする。
与えられた文字列が a
を含んでいれば False
が返され、含んでいなければ True
が返されます。
したがって、 amet
までのすべての単語は True
を返し、結果から取り除かれる。
['amet']
しかし、条件が満たされない場合は、それ以降のすべての要素が取り込まれます。
この例では、 'amet'
要素以降は criteria
に関係なく、すべて含まれることになります。
from itertools import dropwhile
def doesnt_contain_character(str):
substring = 'a'
if substring in str:
return False
else:
return True
string_list = ['lorem', 'ipsum', 'dolor', 'sit', 'amet', 'a', 'b']
print(list(dropwhile(doesnt_contain_character, string_list)))
これは 'amet'
までの要素を削除し、それ以降は削除しないようにします。
['amet', 'a', 'b']
takewhile()関数
takewhile(criteria, sequence)関数は
dropwhile()` の対極にある関数です。
この関数では、関数が失敗しないすべての要素を保存します。
先ほどの例を、ある単語がある文字を含むかどうかをチェックするように書き換えてみましょう。
調べてみましょう。
from itertools import takewhile
def contains_character(str):
substring = 'o'
if substring in str:
return True
else:
return False
string_list = ['lorem', 'ipsum', 'dolor', 'sit', 'amet']
print(list(takewhile(contains_character, string_list)))
['lorem']
2番目の要素で criteria
が失敗するので、 'dolor'
にも o
という文字が含まれているにもかかわらず – それは考慮されません。
groupby()関数
groupby(iterable, key_function)は、同じグループに属する連続した要素を束ねたイテレータを生成する関数である。
ある要素がグループに属するかどうかは、key_function` に依存する。
この関数は、各要素のキー値を計算する。
この場合のキー値とは、特定のグループの ID のことである。
クラスタは終了し、 key_function
が新しい id を返すと、以前からあったものであっても新しく作成される。
もし key_function
が指定されていない場合、デフォルトは ID 関数である。
ただし、重複した値であっても、 – 別のクラスタに分離されている場合は、クラスタ化されないことに注意する必要があります。
from itertools import groupby
word = "aaabbbccaabbbbb"
for key, group in groupby(word):
print(key, list(group))
直感的には、a
とb
のインスタンスはすべて一緒にクラスタリングされると思うかもしれませんが、間にクラスタがあるため、それぞれ別のクラスタに分離されます。
a ['a', 'a', 'a']
b ['b', 'b', 'b']
c ['c', 'c']
a ['a', 'a']
b ['b', 'b', 'b', 'b', 'b']
注意: これを避ける唯一の方法は、キーに基づいてイテラブルを事前にソートすることです。
では、カスタムの key_function
を定義してみましょう。
これはラムダ関数でも専用の関数でもかまいません。
from itertools import groupby
some_list = [("Animal", "cat"),
("Animal", "dog"),
("Animal", "lion"),
("Plant", "dandellion"),
("Plant", "blumen")]
for key, group in groupby(some_list, lambda x : x[0]):
key_and_group = { key : list(group) }
print(key_and_group)
最初の要素は、エントリーが動物か植物かという一般的な分類を表し、2番目の要素は動物名か植物名を表します。
そして、最初の要素に基づいてこれらをグループ化し、一連の各要素を出力しました。
{'Animal': [('Animal', 'cat'), ('Animal', 'dog'), ('Animal', 'lion')]}
{'Plant': [('Plant', 'dandellion'), ('Plant', 'blumen')]}
結論
このガイドでは、Python の組み込みモジュールである itertools
の compress()
, dropwhile()
, takewhile()
, groupby()
の反復処理ツールについて見てきました。
もし、 itertools
モジュールや一般的なイテレータについてもっと学びたいのであれば、他のガイドを参照してください。
- Pythonのイテレーションツール: filter(), islice(), map() and zip()
- Python のイテレータツール – count()、cycle()、chain()