いろいろ備忘録日記

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

Pythonメモ-04 (iterable, iterator, generator, yieldについて)

Pythonやりだすと、いろいろ便利なのでオモシレーってなって、もっと知りたいってなったころに出てくる子達がいます。

iterable, iterator, generator, yield

他の言語やってる人だと大体「あー」って分かるものなのですが

とは言え、最後の yield などは初見「なんて読むんだよコレ」ってなります。

(ちなみに、読み方はカタカナだと「イールド」と言うらしいです。まあ、読めなくても特に問題ないです。)

このあたりの用語や概念は、pythonやる上でとっても大事なのですが

いかんせんとっつきにくいと思います。実際私の周りでもpython勉強しだして、詰まり始めるのがこのあたりでした。

とりあえず日本語にしてみると

そのまんま、日本語にしてしまうと以下のような意味になります。

iterable

反復可能なもの

iterator

反復するもの

generator

生成するもの

yield

生成する

日本語にすると、なんとなーく意味がぼんやりと分かるかも・・・・って感じ。

サンプル

で、実際にコードで表現できないと意味ないので、わかりやすい説明してるところ

とかないのかなと探したら、以下を発見。

stackoverflow.com

stackoverflow の python カテゴリの説明は明確で丁寧なのが多いですね。

(まさに zen of python の精神)

とてもいい情報だったので、自分用のメモとして以下のサンプルつくりました。

        # ----------------------------------------------------------
        # [元ネタ]
        # https://stackoverflow.com/questions/231767/what-does-the-yield-keyword-do-in-python/231855#231855
        #
        # 以下は、元ネタのページ内の記載を自分なりに理解した内容メモです。
        # ----------------------------------------------------------

        # ----------------------------------------------------------
        # 前書き
        # --------------
        # yield が何を行っているのかを理解するためには
        # まず generator が何なのかを理解する必要があります。
        # generator を理解するには、iterable の理解が必要です。
        # てことで、generator の前に iterable から理解していきます。
        # ----------------------------------------------------------

        # ----------------------------------------------------------
        # Iterable
        # --------------
        # list を生成すると、1つずつ要素を読み出すことが出来ます。
        # 一つずつ、要素を読み出すことを「イテレーション」といいます。
        # ----------------------------------------------------------
        list01 = list(range(5))
        for i in list01:
            pr('list-item', i)

        # ----------------------------------------------------------
        # list01 は、iterable です。
        # リスト内包表記(list comprehension)を使ったとき
        # リストが生成され、これも iterable なものとなります。
        # ----------------------------------------------------------
        list02 = [x * 2 for x in range(5)]
        for i in list02:
            pr('list-item(list comprehension)', i)

        # ----------------------------------------------------------
        # for ... in ... は、iterableなもの全てに使えます。
        # (リストや文字列や辞書や集合やファイルなど・・・)
        #
        # このように iterable なオブジェクトはとても使いやすく便利
        # なのですが、全ての値をメモリに保持してしまうという面も持っています。
        #
        # 例えば、大量なデータを持つリストなどです。このような時
        # 実際に全てのデータが必要では無い場合もあります。
        # 利用しないデータの割合が多い場合、多くは不必要にメモリにロードされ
        # 空きメモリを圧迫します。このような場合に generator が登場します。
        # ----------------------------------------------------------

        # ----------------------------------------------------------
        # Generators
        # --------------
        # Generators は イテレータです。(iterator => iterate するもの)
        # でも、一回しかイテレートすることが出来ません。
        # (つまり、一回全要素をループさせると終わり)
        #
        # これは、generatorが一度に全ての要素をメモリに持つのではなく
        # 必要な要素をその都度 (on the fly) 生成するからです。
        #
        # なので、generator (generate するもの) となります。
        #
        # generator を生成する場合、() で生成部分を囲みます。
        # 先ほどのリスト内包表記を generator に変更する場合
        # [] を () に変えるだけで generator になります。
        # ----------------------------------------------------------
        gen01 = (x * 2 for x in range(5))
        pr('generator-object', gen01)

        for i in gen01:
            pr('generator-item(first-loop)', i)
        else:
            pr('first-loop', 'done')

        for i in gen01:
            pr('generator-item(second-loop)', i)
        else:
            pr('second-loop', 'done')

        # ----------------------------------------------------------
        # Yield
        # --------------
        # ここでやっと、yield の登場です。
        # yield は return のような感じで使います。
        # return は、値を返しますが、yield は generator を返します。
        #
        # とりあえず、以下に yield のサンプルを記載します。
        # 下記を実行すると、5回値を生成します。
        # ----------------------------------------------------------
        def create_generator():
            yield 1
            yield 2
            yield 3
            yield 4
            yield 5

        gen02 = create_generator()
        pr('generator-object', gen02)

        for x in gen02:
            pr('generator-item', x)

        # -----------------------------------------------------------------------
        # 上記の create_generator 関数では yield が5回登場しています。
        # なので、取得した generator をループすると 5回値が出力されます。
        #
        # このように書くと、なんとなく分かるけど、なんとなく分からないって
        # なってしまうのが、yield のややこしいところです。
        #
        # 大事なのが、以下の点です。
        # 「関数内に yield が存在すると、python は 関数本体を実行せずに
        # generator を生成して、呼び元に返す。」
        #
        # 内部に yield が利用されている関数は、呼ぶとすぐには実行されません。
        # generator オブジェクトが生成されて、それが返ってきます。
        #
        # なので、
        #   gen02 = create_generator()
        # と書くと、create_generator関数の中では yield があるので
        # python が内部で generator を生成して返してくれます。
        # それが 変数 gen02 にセットされます。
        #
        # generator は イテレートさせないと処理が進みません。
        # 呼び出される度(つまり一回分イテレートされるたび)に、yield一つ分進み、次の
        # yield が見つかったタイミングで一時停止します。
        #
        # なので、上のサンプルでは ループが一回進む度に yield も一つずつ進んでいって
        # 最終的に yield が5回分呼ばれたらループ終了となります。(StopIterationが発生します。)
        #
        # 一度使い切った generator は利用できないので、上記サンプルの gen02 を
        # 再度 for 文で利用しても、今度は一回もループされません。
        # (yield 5 まで進んでしまっているので、次の yield がもうないため)
        #
        # yield の動き自体はシンプルなので、以下のように無限ループの中で yield すると
        # 止めない限り、永遠に値を生成するようにも出来ます。
        # ------------------------------------------------------------------------
        def gen_forever():
            while True:
                yield datetime.now()

        gen03 = gen_forever()
        for i, d in enumerate(gen03):
            if i > 5:
                break
            pr('gen03', d.isoformat())
            time.sleep(1)

実行すると、例として以下のようになります。

Connected to pydev debugger (build 171.4249.47)
list-item=0
list-item=1
list-item=2
list-item=3
list-item=4
list-item(list comprehension)=0
list-item(list comprehension)=2
list-item(list comprehension)=4
list-item(list comprehension)=6
list-item(list comprehension)=8
generator-object=<generator object Sample.exec.<locals>.<genexpr> at 0x0000000004C3B7D8>
generator-item(first-loop)=0
generator-item(first-loop)=2
generator-item(first-loop)=4
generator-item(first-loop)=6
generator-item(first-loop)=8
first-loop='done'
second-loop='done'
generator-object=<generator object Sample.exec.<locals>.create_generator at 0x0000000004BE48E0>
generator-item=1
generator-item=2
generator-item=3
generator-item=4
generator-item=5
gen03='2017-05-02T18:46:34.268324'
gen03='2017-05-02T18:46:35.268381'
gen03='2017-05-02T18:46:36.268438'
gen03='2017-05-02T18:46:37.268496'
gen03='2017-05-02T18:46:38.268553'
gen03='2017-05-02T18:46:39.268610'

Process finished with exit code 0

サンプルのソースコードは以下でも見れます。

github.com


以下、参考にした情報です。

下記のページには、こんな記事よりももっと詳しく分かりやすく記載されています。


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

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