いろいろ備忘録日記

主に .NET とか Go とか Flutter とか Python絡みのメモを公開しています。

Pythonメモ-72 (ライブラリメモ - itertools) (zip_longest, chain, groupby, accumulate, islice, tee)

概要

標準ライブラリのitertoolsについてのメモです。

先日、以下の記事を発見。

medium.com

とても分かりやすく纏められていたので、ついでに自分のメモも書こうってなりました。

以下、それぞれの関数についての個人メモです。

上記サイトの内容の方がもっとわかりやすいので、ここから下は見る必要ないかもですw

itertoolsとは

itertoolsモジュールは、イテレータを構築するための関数が集まっているモジュールです。

有名なやつだと、zip_longestとかgroupbyとかですかね。

元のシーケンスに対して、ある条件を満たしているもののみを返すイテレータとか、無限イテレータとかを操作できます。

他の言語やってる人、たとえばC#とかJavaだと、似たようなものにLINQstreamがありますが

感覚的にitertools + 内包表記LINQstreamに近いって感じで捉えるとわかりやすいカモ。

このモジュールが使えるようになると、シーケンスの処理がぐっと楽になります。

itertoolsモジュールの各関数

以下自分のメモコードをそのまま貼り付けていますので、順不同です。

なお、予め以下を実行している状態です。

import itertools as it
import operator as op
import pandas as pd

itertools.chain

        # -----------------------------------------------
        # itertools.chain()
        # ----------------------
        # 複数のシーケンスを繋いで一つのようにする。
        # -----------------------------------------------
        hr('it.chain()')

        list01 = ['hello', 'world']
        list02 = list(range(10))

        for x in it.chain(list01, list02, 'abc'):
            pr('value', x)

結果は

----------------it.chain()----------------
value='hello'
value='world'
value=0
value=1
value=2
value=3
value=4
value=5
value=6
value=7
value=8
value=9
value='a'
value='b'
value='c'

try-python/itertools01.py at master · devlights/try-python · GitHub

itertools.zip_longest

        # -----------------------------------------------
        # itertools.zip_longest()
        # ----------------------
        # 組み込み関数 zip() の別バージョン
        # 要素数が多い方に合わせる。
        #
        # zip() は、要素が最も少ない方になる。
        # -----------------------------------------------
        hr('it.zip_longest()')

        for x, y in zip(list01, list02):
            pr('zip()', (x, y))

        # zip_longest()は、要素が最も多い方になる
        for x, y in it.zip_longest(list01, list02):
            pr('zip_longest()', (x, y))

結果は

----------------it.zip_longest()----------------
zip()=('hello', 0)
zip()=('world', 1)
zip_longest()=('hello', 0)
zip_longest()=('world', 1)
zip_longest()=(None, 2)
zip_longest()=(None, 3)
zip_longest()=(None, 4)
zip_longest()=(None, 5)
zip_longest()=(None, 6)
zip_longest()=(None, 7)
zip_longest()=(None, 8)
zip_longest()=(None, 9)

try-python/itertools01.py at master · devlights/try-python · GitHub

itertools.cycle

        # -----------------------------------------------
        # itertools.cycle()
        # ----------------------
        # itertools.cycleは、名前の通り
        # 最初に指定したシーケンスを繰り返しサイクルする
        # イテレータを生成してくれる。
        #
        # これで生成したイテレータは無限にサイクルするので
        # ストップさせるのは、利用者側の責任になる。
        # -------------------------------------------
        numbers = list(range(0, 9))
        cycle_iter = it.cycle((1, 2, 3))

        hr('itertools.cycle()')

        for i, j in zip(numbers, cycle_iter):
            pr("i,j", f'{i}-{j}')

結果は

----------------itertools.cycle()----------------
i,j='0-1'
i,j='1-2'
i,j='2-3'
i,j='3-1'
i,j='4-2'
i,j='5-3'
i,j='6-1'
i,j='7-2'
i,j='8-3'

try-python/itertools02.py at master · devlights/try-python · GitHub

itertools.repeat

        # -----------------------------------------------
        # itertools.repeat()
        # ----------------------
        # itertools.repeat() は、指定したオブジェクトを
        # 指定した回数分繰り返すイテレータを生成してくれる。
        # 繰り返し回数は、第二引数の times で指定できる。
        # timesのデフォルトは None となっており、これは
        # 無限の繰り返しを表す。
        # -----------------------------------------------
        hr('it.repeat()')

        str01 = 'hello python'
        for x in it.repeat(str01, 2):
            pr('it-repeat', x)

        list01 = list(range(5))
        for i, x in enumerate(it.repeat(list01)):
            # そのままだと、無限ループするので10回出力で止める
            if i >= 10:
                break
            pr('it-repeat times=None', f'{i} -- {x}')

結果は

----------------it.repeat()----------------
it-repeat='hello python'
it-repeat='hello python'
it-repeat times=None='0 -- [0, 1, 2, 3, 4]'
it-repeat times=None='1 -- [0, 1, 2, 3, 4]'
it-repeat times=None='2 -- [0, 1, 2, 3, 4]'
it-repeat times=None='3 -- [0, 1, 2, 3, 4]'
it-repeat times=None='4 -- [0, 1, 2, 3, 4]'
it-repeat times=None='5 -- [0, 1, 2, 3, 4]'
it-repeat times=None='6 -- [0, 1, 2, 3, 4]'
it-repeat times=None='7 -- [0, 1, 2, 3, 4]'
it-repeat times=None='8 -- [0, 1, 2, 3, 4]'
it-repeat times=None='9 -- [0, 1, 2, 3, 4]'

try-python/itertools03.py at master · devlights/try-python · GitHub

itertools.count

        # -----------------------------------------------
        # itertools.count()
        # ----------------------
        # 無限イテレータ。
        # 指定した start 値から step 分加算した値を
        # 生成し続ける。
        # -----------------------------------------------
        hr('itertools.count()')

        iter01 = it.count(start=10, step=2)
        for i in iter01:
            if i > 20:
                break
            pr('it.count', i)

結果は

----------------itertools.count()----------------
it.count=10
it.count=12
it.count=14
it.count=16
it.count=18
it.count=20

try-python/itertools04.py at master · devlights/try-python · GitHub

itertools.accumulate

        # -----------------------------------------------
        # itertools.accumulate()
        # ----------------------
        # 指定した関数の結果を累積した結果を返す。
        # 汎用性が高い関数。seed は指定できない。
        # 最終的な累積結果のみが欲しい場合はfunctools.reduce() を使う。
        #
        # 操作するための関数を指定しない場合、デフォルトは加算(operator.add)となる
        # 操作関数は引数を二つ受け取るものを指定する必要がある
        # 一つ目の引数に現在の累積値、二つ目に次の項目が入る。
        # 以下のようなイメージとなる
        #
        # list(itertools.accumlate([1, 2, 3, 4, 5]))
        #
        # operator.add(1, 2)  ==> 3
        # operator.add(3, 3)  ==> 6
        # operator.add(6, 4)  ==> 10
        # operator.add(10, 5) ==> 15
        #
        # なので、結果は[1, 3, 6, 10, 15]となる
        # -----------------------------------------------
        hr('itertools.accumulate()')

        data01 = [1, 2, 3, 4, 5]

        # 加算
        pr('it.accumulate', list(it.accumulate(data01, op.add)))
        # 乗算
        pr('it.accumulate', list(it.accumulate(data01, op.mul)))

        # 自前関数
        def _accum(accum: int, curr: int) -> int:
            return accum * curr if accum < 20 else 20

        pr('it.accumulate', list(it.accumulate(data01, func=_accum)))

結果は

----------------itertools.accumulate()----------------
it.accumulate=[1, 3, 6, 10, 15]
it.accumulate=[1, 2, 6, 24, 120]
it.accumulate=[1, 2, 6, 24, 20]

try-python/itertools04.py at master · devlights/try-python · GitHub

itertools.compress

        # -----------------------------------------------
        # itertools.compress()
        # ----------------------
        # 引数に指定されたシーケンスを第二引数で指定したリスト
        # 内でTrueとなっているもののみにフィルタリングする。
        # 注意点として、zip関数と同様に短い方が終了した
        # 段階で処理が打ち切られる。
        # -----------------------------------------------
        hr('itertools.compress()')

        selector01 = [True if x >= 3 else False for x in data01]
        pr('it.compress', list(it.compress(data01, selectors=selector01)))

結果は

----------------itertools.compress()----------------
it.compress=[3, 4, 5]

try-python/itertools04.py at master · devlights/try-python · GitHub

itertools.dropwhile

        # -----------------------------------------------
        # itertools.dropwhile()
        # ----------------------
        # 条件が成り立つ間、要素を捨てる。
        # 条件が成り立たなくなってからの要素が返る。
        # 他の言語では、SkipWhile() という名前になったりする。
        # 第一引数が predicate つまり、条件。
        # 第二引数が データ であることに注意。
        # -----------------------------------------------
        hr('itertools.dropwhile()')
        pr('it.dropwhile', list(it.dropwhile(lambda x: x < 3, data01)))

結果は

----------------itertools.dropwhile()----------------
it.dropwhile=[3, 4, 5]

try-python/itertools04.py at master · devlights/try-python · GitHub

itertools.filterfalse

        # -----------------------------------------------
        # itertools.filterfalse()
        # ----------------------
        # ちょっと特殊な子で、条件がfalseとなるものが返る。
        # predicate に None が指定できる。Noneを指定した
        # 場合は、データの要素が false 判定されたものが返る。
        # 第一引数が predicate つまり、条件。
        # 第二引数が データ であることに注意。
        # -----------------------------------------------
        hr('itertools.filterfalse()')
        pr('it.filterfalse', list(it.filterfalse(lambda x: x < 3, data01)))

結果は

----------------itertools.filterfalse()----------------
it.filterfalse=[3, 4, 5]

try-python/itertools04.py at master · devlights/try-python · GitHub

dropwhileとfilterfalseの違い

        # ----------------------------------------------------------------------------
        # 上の dropwhile と filterfalse では同じ条件を指定している。
        # dropwhile は predicate が True の要素は飛ばして、falseになってから要素を返す。
        # filterfalse は predicate が false の要素を返す。
        # つまり、上の2つは対象データが[1, 2, 3, 4, 5]の場合は、同じ結果となる。
        #
        # 違いが出るのは、一旦 True なデータが出現した後に 再度 false データが出現した場合
        #
        # dropwhile() は、一度 True になった後は、以降のデータは無条件で全部返る。
        # filterfalse() は、false の判定になったものしか返さない。
        # ----------------------------------------------------------------------------
        hr('difference between dropwhile() and filterfalse()')
        data02 = [*data01, 2, 1, 3, 4, 5]
        pr('it.dropwhile', list(it.dropwhile(lambda x: x < 3, data02)))
        pr('it.filterfalse', list(it.filterfalse(lambda x: x < 3, data02)))

結果は

----------------difference between dropwhile() and filterfalse()----------------
it.dropwhile=[3, 4, 5, 2, 1, 3, 4, 5]
it.filterfalse=[3, 4, 5, 3, 4, 5]

try-python/itertools04.py at master · devlights/try-python · GitHub

itertools.groupby

        # -----------------------------------------------
        # itertools.groupby()
        # ----------------------
        # 指定された条件でグルーピングを行う。
        # groupby() は、key で指定した関数が返す値が
        # 変わる度に新たなグループを生成するため
        # 通常指定するシーケンスはソート済みである必要がある。
        #
        # また、groupby() が返すデータはそれ自身もイテレータ
        # となっているため、データが後の処理で必要な場合は
        # 別途リストなどを用意して保持しておく必要がある。
        # (それか、再度 groupby() 呼ぶ)
        # -----------------------------------------------
        GroupingData = collections.namedtuple('GroupingData', ['id', 'name'])

        # 未ソートのシーケンス
        groups_not_sorted = [
            GroupingData(1, 'data1'),
            GroupingData(1, 'data2'),
            GroupingData(2, 'data3'),
            GroupingData(1, 'data4')
        ]

        def key_func(grp: GroupingData) -> int:
            return grp.id

        hr('未ソートの状態で groupby() ')

        grp_iter = it.groupby(groups_not_sorted, key=key_func)
        for k, g in grp_iter:
            pr('grp-key', k)
            pr('\tgrp-items', ','.join(_.name for _ in g))

        hr('ソート済みの状態で groupby() ')

        # ソート済み
        groups_sorted = sorted(groups_not_sorted, key=key_func)

        grp_iter = it.groupby(groups_sorted, key=key_func)
        for k, g in grp_iter:
            pr('grp-key', k)
            pr('\tgrp-items', ','.join(_.name for _ in g))

結果は

----------------未ソートの状態で groupby() ----------------
grp-key=1
    grp-items='data1,data2'
grp-key=2
    grp-items='data3'
grp-key=1
    grp-items='data4'
----------------ソート済みの状態で groupby() ----------------
grp-key=1
    grp-items='data1,data2,data4'
grp-key=2
    grp-items='data3'

try-python/itertools05.py at master · devlights/try-python · GitHub

itertools.islice

        # -----------------------------------------------
        # itertools.islice()
        # ----------------------
        # 指定されたイテレータブルに対してスライスを行う。
        # islice() には、キーワード引数が存在せず位置引数のみ。
        # なので、値の指定数で挙動が変わる。
        # 戻り値も当然イテレータとなっている。
        #
        # 引数が一つの場合、stopが指定された状態となる。
        # 引数が二つの場合、start, stopが指定された状態となる。
        # 引数が三つの場合、start, stop, stepが指定された(略
        # -----------------------------------------------
        hr('it.islice()')

        data01 = list(range(10))
        pr('it.islice(8)', list(it.islice(data01, 8)))
        pr('it.islice(5,8)', list(it.islice(data01, 5, 8)))
        pr('it.islice(2,8,2)', list(it.islice(data01, 2, 8, 2)))

        # stop には、None が指定可能。None を指定した場合は最後までという意味となる。
        pr('it.islice(5,None)', list(it.islice(data01, 5, None)))

結果は

----------------it.islice()----------------
it.islice(8)=[0, 1, 2, 3, 4, 5, 6, 7]
it.islice(5,8)=[5, 6, 7]
it.islice(2,8,2)=[2, 4, 6]
it.islice(5,None)=[5, 6, 7, 8, 9]

try-python/itertools06.py at master · devlights/try-python · GitHub

itertools.starmap

        # -----------------------------------------------
        # itertools.starmap()
        # ----------------------
        # グループ化済みの iterable に対して function を適用する。
        # 例えば、zipした後の結果に対して、更に function 適用するなど。
        #
        # つまり、以下のような感じ。
        #
        # l1 = [1, 2, 3]
        # l2 = [9, 8, 7]
        # l3 = list(zip(l1, l2)) ==> [(1,9), (2,8), (3,7)]
        #
        # l3に対して operator.add で starmapする
        # list(itertools.starmap(operator.add, l3))
        #
        # 結果は [10, 10, 10] となる。
        # つまり、以下を実施したのと同じこと
        #
        # for item in l3:
        #     operator.add(*item)
        #
        # なので、名前が starmap となっている
        # -----------------------------------------------
        hr('it.starmap()')

        list01 = [9, 8, 7]
        list02 = [1, 2, 3]
        list03 = list(zip(list01, list02))

        starmap = it.starmap(ope.sub, list03)
        pr('it.starmap', list(starmap))

        list04 = list(zip(list01, list02, *list03))
        pr('it.starmap', list(it.starmap(lambda *args: sum(args), list04)))

結果は

----------------it.starmap()----------------
it.starmap=[8, 6, 4]
it.starmap=[34, 16]

try-python/itertools07.py at master · devlights/try-python · GitHub

itertools.takewhile

        # -----------------------------------------------
        # itertools.takewhile()
        # ----------------------
        # 指定した条件を満たす間、要素を返す。
        # dropwhile() の 逆。
        #
        # なので、一度でも条件から外れた場合、それ以降に
        # 条件を満たす値があっても要素は返らない。
        # -----------------------------------------------
        hr('it.takewhile()')

        list05 = sorted(it.chain(list01, list02))
        pr('list05', list05)

        takewhile = it.takewhile(lambda x: x < 5, list05)
        pr('it.takewhile', list(takewhile))

結果は

----------------it.takewhile()----------------
list05=[1, 2, 3, 7, 8, 9]
it.takewhile=[1, 2, 3]

try-python/itertools07.py at master · devlights/try-python · GitHub

itertools.tee

        # -----------------------------------------------
        # itertools.tee()
        # ----------------------
        # 指定された iterable を複数の独立した iterable にして返す。
        # つまり、n=2 とすると、元の iterable を複製した
        # 二つの iterable が取得できる。(tuple(iterable, iterable))
        #
        # 公式ドキュメントに記載されているように、一度 tee() を
        # 使用して分割した original iterable は、内部状態を共有しているので
        # もう別の場所では利用しないほうがいい。
        #
        # 引用:
        # Once tee() has made a split,
        # the original iterable should not be used anywhere else;
        # otherwise, the iterable could get advanced
        # without the tee objects being informed.
        # -----------------------------------------------
        hr('it.tee()')

        list06 = list('helloworld')

        it_tee = it.tee(list06, 2)
        it_asc, it_desc = it_tee[0], reversed(list(it_tee[-1]))
        for it01, it02 in zip(it_asc, it_desc):
            pr('it.tee', f'{it01}, {it02}')

結果は

----------------it.tee()----------------
it.tee='h, d'
it.tee='e, l'
it.tee='l, r'
it.tee='l, o'
it.tee='o, w'
it.tee='w, o'
it.tee='o, l'
it.tee='r, l'
it.tee='l, e'
it.tee='d, h'

try-python/itertools07.py at master · devlights/try-python · GitHub

itertools.product

        # -----------------------------------------------
        # itertools.product()
        # ----------------------
        # デカルト積を求める。とドキュメントに記載されているが
        # 要は list01 と list02 が存在する場合
        #     (x, y) for x in list01 for y in list02
        # と同じことになる。
        #
        # キーワード引数の repeat を利用すると直積も求められる。
        # -----------------------------------------------
        hr('it.product()')

        list01 = list('ABC')
        list02 = list('DEF')

        pr('it.product(*iterable)', list(it.product(list01, list02)))
        pr('listcomp', [(x, y) for x in list01 for y in list02])

        pr('it.product(*iterable, repeat=2)', list(it.product('AB', repeat=4)))

        # テストデータを一気に生成するときに便利
        date_range = pd.date_range(dt.date.today(), periods=3, freq='D')
        values = (10, 11, 12)

        pr('it.product', list(it.product(date_range, values)))
        pr('listcomp', [(d, v) for d in date_range for v in values])  # 同じ

結果は

----------------it.product()----------------                     
it.product(*iterable)=[('A', 'D'),                               
 ('A', 'E'),                                                     
 ('A', 'F'),                                                     
 ('B', 'D'),                                                     
 ('B', 'E'),                                                     
 ('B', 'F'),                                                     
 ('C', 'D'),                                                     
 ('C', 'E'),                                                     
 ('C', 'F')]                                                     
listcomp=[('A', 'D'),                                            
 ('A', 'E'),                                                     
 ('A', 'F'),                                                     
 ('B', 'D'),                                                     
 ('B', 'E'),                                                     
 ('B', 'F'),                                                     
 ('C', 'D'),                                                     
 ('C', 'E'),                                                     
 ('C', 'F')]                                                     
it.product(*iterable, repeat=2)=[('A', 'A', 'A', 'A'),           
 ('A', 'A', 'A', 'B'),                                           
 ('A', 'A', 'B', 'A'),                                           
 ('A', 'A', 'B', 'B'),                                           
 ('A', 'B', 'A', 'A'),                                           
 ('A', 'B', 'A', 'B'),                                           
 ('A', 'B', 'B', 'A'),                                           
 ('A', 'B', 'B', 'B'),                                           
 ('B', 'A', 'A', 'A'),                                           
 ('B', 'A', 'A', 'B'),                                           
 ('B', 'A', 'B', 'A'),                                           
 ('B', 'A', 'B', 'B'),                                           
 ('B', 'B', 'A', 'A'),                                           
 ('B', 'B', 'A', 'B'),                                           
 ('B', 'B', 'B', 'A'),                                           
 ('B', 'B', 'B', 'B')]                                           
it.product=[(Timestamp('2018-03-29 00:00:00', freq='D'), 10),    
 (Timestamp('2018-03-29 00:00:00', freq='D'), 11),               
 (Timestamp('2018-03-29 00:00:00', freq='D'), 12),               
 (Timestamp('2018-03-30 00:00:00', freq='D'), 10),               
 (Timestamp('2018-03-30 00:00:00', freq='D'), 11),               
 (Timestamp('2018-03-30 00:00:00', freq='D'), 12),               
 (Timestamp('2018-03-31 00:00:00', freq='D'), 10),               
 (Timestamp('2018-03-31 00:00:00', freq='D'), 11),               
 (Timestamp('2018-03-31 00:00:00', freq='D'), 12)]               
listcomp=[(Timestamp('2018-03-29 00:00:00', freq='D'), 10),      
 (Timestamp('2018-03-29 00:00:00', freq='D'), 11),               
 (Timestamp('2018-03-29 00:00:00', freq='D'), 12),               
 (Timestamp('2018-03-30 00:00:00', freq='D'), 10),               
 (Timestamp('2018-03-30 00:00:00', freq='D'), 11),               
 (Timestamp('2018-03-30 00:00:00', freq='D'), 12),               
 (Timestamp('2018-03-31 00:00:00', freq='D'), 10),               
 (Timestamp('2018-03-31 00:00:00', freq='D'), 11),               
 (Timestamp('2018-03-31 00:00:00', freq='D'), 12)]               

try-python/itertools08.py at master · devlights/try-python · GitHub

itertools.permutations

        # -----------------------------------------------
        # itertools.permutations()
        # ----------------------
        # 順列を生成する。(e.g. 5! = 5*4*3*2*1 = 120)
        # 第二引数に順列の長さを指定できる。省略可能
        # 長さにNone(これがデフォルト)を指定すると
        # 可能な最長の順列が生成される。
        # -----------------------------------------------
        hr('it.permutations()')
        pr('it.permutations(r=None)...(3!=3*2*1)', list(it.permutations('ABC')))

        # 長さを指定することで、シーケンスからいくつ選ぶのかを指定できる
        pr('it.permutations(r=3)...(4p3=4*3*2)', len(list(it.permutations('ABCD', r=3))))
        pr('it.permutations(r=2)...(4p2=4*3)', len(list(it.permutations('ABCD', r=2))))

結果は

----------------it.permutations()----------------
it.permutations(r=None)...(3!=3*2*1)=[('A', 'B', 'C'),
 ('A', 'C', 'B'),
 ('B', 'A', 'C'),
 ('B', 'C', 'A'),
 ('C', 'A', 'B'),
 ('C', 'B', 'A')]
it.permutations(r=3)...(4p3=4*3*2)=24
it.permutations(r=2)...(4p2=4*3)=12

try-python/itertools08.py at master · devlights/try-python · GitHub

itertools.combinations, combinations_with_replacement

        # -----------------------------------------------
        # itertools.combinations()
        # ----------------------
        # 組合せを生成する。
        # 第二引数は必須。
        #
        # 並べる際の順序を無視した結果を返す。
        # つまり、(a, b, c)と(b, a, c)と(c, b, a)は同じとみなす。
        # -----------------------------------------------
        hr('it.combinations()')
        pr('it.combinations(r=2)...(4c2=4p2/2!)', list(it.combinations('ABCD', r=2)))
        pr('it.combinations(r=2)...(4c2=4p2/2!)', len(list(it.combinations('ABCD', r=2))))

        # combinations_with_replacement() は、それぞれの要素が重複することを許す
        pr('it.combinations_with_replacement(r=2)', list(it.combinations_with_replacement('ABCD', r=2)))
        pr('it.combinations_with_replacement(r=2)', len(list(it.combinations_with_replacement('ABCD', r=2))))

結果は

----------------it.combinations()----------------
it.combinations(r=2)...(4c2=4p2/2!)=[('A', 'B'), ('A', 'C'), ('A', 'D'), ('B', 'C'), ('B', 'D'), ('C', 'D')]
it.combinations(r=2)...(4c2=4p2/2!)=6
it.combinations_with_replacement(r=2)=[('A', 'A'),
 ('A', 'B'),
 ('A', 'C'),
 ('A', 'D'),
 ('B', 'B'),
 ('B', 'C'),
 ('B', 'D'),
 ('C', 'C'),
 ('C', 'D'),
 ('D', 'D')]
it.combinations_with_replacement(r=2)=10

try-python/itertools08.py at master · devlights/try-python · GitHub


過去の記事については、以下のページからご参照下さい。

サンプルコードは、以下の場所で公開しています。