python ジェネレータとイテレーターについてまとめる(備忘録)

## そもそもジェネレータとは
><b>ジェネレータ</b>　…イテラブルなオブジェクトの一種で、文字列やリストなどと同様にfor文を使い値を1つずつ取り出すことができる。
⇒あらかじめ複数の値を格納するのではなく、値が要求されるたびに一個ずつ要素を生成する。
⇒多数の値を格納しておく必要がないので、<b>メモリの消費量を抑えることができ</b>たり、<b>無限の個の値を生成することができる</b>利点を持つ。
⇒<b>ジェネレータ式</b>と<b>ジェネレータ関数</b>がある。

><b>イテラブルとは</b>
 ⇒for文などで要素を1つずつ取り出して処理できるようなオブジェクトのこと
参考
[pythonプログラミングtips集/【python】イテラブルオブジェクトとは](https://science-log.com/python%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0tips%E9%9B%86/%E3%80%90python%E3%80%91%E3%82%A4%E3%83%86%E3%83%A9%E3%83%96%E3%83%AB%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%A8%E3%81%AF/)

><b>イテレータ</b>
⇒イテレータとは、イテラブル（繰り返し可能）なオブジェクトから要素を取り出すオブジェクトのことです。仮に、for i in some_listと書いたときfor文は、some_list(listオブジェクト)に実装されている特殊メソッドiter()を呼び出して、その戻り値を使います。この戻り値を<b>イテレータ</b>と呼びます。for文では、inの後に指定されたオブジェクトには、必ずiter()を適応する。
参考
[イテレータとイテラブルについて（備忘録）](https://qiita.com/1023shogo2013/items/61a93a3c3c407ea3c522)

~~~python
# ジェネレータ式
J = (x * x for x in range(10))
print(J)

# タプルの内包表記
T = tuple(x * x for x in range(10))
print(T)

# リストの内包表記
L = [x * x for x in range(10)]
print(L)

>>> 
 # ジェネレータは、呼び出されると実際の作業をせずイテレータを返す。 
<generator object <genexpr> at 0x00000293BFCE9510>
# タプルの内包表記
(0, 1, 4, 9, 16, 25, 36, 49, 64, 81)
# リストの内包表記
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
~~~
~~~python
# ジェネレータ式
J = (x * x for x in range(10))

"""
next関数を用いて要素を取り出していく。
 膨大な量のデータから要素を取り出す際に、一度にすべて取り出さなくてもよく、
 必要な時に値を生成して取り出せるので、メモリや速度的に優位。
"""
print(next(J))
print(next(J))
print(next(J))
print(next(J))

>>>
0
1
4
9
~~~
~~~python
# ジェネレータ関数
def func():
    for i in range(10):
        yield x * x

print(func())

>>><generator object func at 0x0000022BC74B9510>

# ジェネレータ関数のネスト
def func():
    for x in range(10):
        yield x * x

for y in func():
    print(y, end=' ')

>>> 0 1 4 9 16 25 36 49 64 81 
~~~

## ジェネレータ関数を使ってコードを簡潔に！
~~~python
# ジェネレータ関数なし
def j_func(numbers):
    result = []
    for index, letter in enumerate(numbers):
        if letter == ' ':
            result.append(index + 1)
    return result

numbers = 'zero one two three four five six seven eight nine ten'
result = j_func(numbers)
print(result[:10])
~~~
~~~python
# ジェネレータ関数あり
def j_func(numbers):
    result = []
    for index, letter in enumerate(numbers):
        if letter == ' ':
            yield index + 1 

numbers = 'zero one two three four five six seven eight nine ten'

# ジェネレータ関数が呼び出されると、
# 作業を実行せず、イテレータを返す
J = j_func(numbers) 

# next関数を呼び出すごとにイテレーターは
# yield式で要素を一つ返す
print(next(J))
print(next(J))
print(next(J))

>>>
5
9
13
~~~
<b>ジェネレータを関数を使って、簡潔にわかりやすく、メモリの消費量を抑えて、コードを書くことができる</b>

## ジェネレータの注意点
ジェネレータをnext関数で値を生成するとその値は削除されてしまう。
値がなくなった状態でnext()を行うとStopIterationが出力される。
~~~python
def j_func():
    forリスト x in range(5):
        yield x * x

J = j_func()
print(next(J))
print(next(J))
print(next(J))
print(next(J))
print(next(J)) # 5回目
print(next(J)) # StopIteration

>>>
0
1
4
9
16

stopIteration # 例外
~~~

## データ量が膨大な場合はリスト内包表記ではなく、ジェネレータ式を使おう！
以下の内包表記の例とジェネレータの例を比べてみよう！
速度やメモリ消費量が圧倒的に違う。ジェネレータ式は連鎖的に組み合わせることができて、圧倒的速さで動作してくれる。
~~~python
# リスト内包表記
J = [x for x in range(1, 100000000)]
ans = [y ** y for y in J]
print(next(ans))
print(next(ans))
print(next(ans))
print(next(ans))
print(next(ans))
~~~

~~~python
# ジェネレータ式
J = (x for x in range(1, 100000000))
ans = (y ** y for y in J)
print(next(ans))
print(next(ans))
print(next(ans))
print(next(ans))
print(next(ans))

>>>
1
4
27
256
3125
~~~

## yield from式について
yield from式は、yield文を使って値を返す代わりに指定したジェネレータに値を返させることができる。
~~~python
import random

def func1(j):
    for i in range(j):
        yield i * i

def func2():
    j = random.randint(1, 5)
    yield from func1(j)

F = func2()
print(next(F))
print(next(F))
print(next(F))


# 何回かやってみよう。jの値によって、例外が発生したり、しなかったり
>>> 
0
1
Traceback (most recent call last):
  File "c:\Users\reon1\vscode_project\atcoder\a.py", line 14, in <module>  
    print(next(F))
StopIteration
~~~

## 組み込み関数itertoolsについて
参照
[公式ドキュメント　itertools](https://docs.python.org/ja/3/library/itertools.html)

# chain
イテレータをつなげて1つのシーケンスへ
~~~python
import itertools

it = itertools.chain([1, 2, 3], [4, 5, 6])
print(list(it))

>>>[1, 2, 3, 4, 5, 6]
~~~

# repeat
一つの値を何度も出力する
~~~python
import itertools

it = itertools.repeat('hi', 5)
print(list(it))

>>>['hi', 'hi', 'hi', 'hi', 'hi']
~~~

# cycle
同じイテレータ要素を繰り返す。
~~~python
import itertools

it = itertools.cycle([100, 10, 1])
print([next(it) for _ in range(10)])

>>>[100, 10, 1, 100, 10, 1, 100, 10, 1, 100]
~~~
# permutations
イテレータのからN個の要素を取り出してできる順序を返す
第一引数にイテラブル、第二引数に何個組み合わせるか指定する
~~~python
import itertools

it = itertools.permutations([1, 2, 3, 4, 5], 3)
print(list(it))

>>>
[(1, 2, 3), (1, 2, 4), (1, 2, 5), (1, 3, 2), (1, 3, 4), (1, 3, 5), (1, 4, 2), (1, 4, 3), (1, 4, 5), (1, 5, 2), (1, 5, 3), (1, 5, 4), (2, 1, 3), (2, 1, 4), (2, 1, 5), (2, 3, 1), (2, 3, 4), (2, 3, 5), (2, 4, 1), (2, 4, 3), (2, 4, 5), (2, 5, 1), (2, 5, 3), (2, 5, 4), (3, 1, 2), (3, 1, 4), (3, 1, 5), (3, 2, 1), (3, 2, 4), (3, 2, 5), (3, 4, 1), (3, 4, 2), (3, 4, 5), (3, 5, 1), (3, 5, 2), (3, 5, 4), (4, 1, 2), (4, 1, 3), (4, 1, 5), (4, 2, 1), (4, 2, 3), (4, 2, 5), (4, 3, 1), (4, 3, 2), (4, 3, 5), (4, 5, 1), (4, 5, 2), (4, 5, 3), (5, 1, 2), (5, 1, 3), (5, 1, 4), (5, 2, 1), (5, 2, 3), (5, 2, 4), (5, 3, 1), (5, 3, 2), (5, 3, 4), (5, 4, 1), (5, 4, 2), (5, 4, 3)]
~~~

# combinations
イテレータからN個の要素を取り出したときに可能となるすべての組み合わせを順不同で返す
~~~python
import itertools

it = itertools.combinations([1, 2, 3, 4, 5], 3)
print(list(it))

>>>[(1, 2, 3), (1, 2, 4), (1, 2, 5), (1, 3, 4), (1, 3, 5), (1, 4, 5), (2, 3, 4), (2, 3, 5), (2, 4, 5), (3, 4, 5)]
~~~

itertoolsの一部を書き出したが、まだまだ便利なものが多く存在する。

## 参考書籍
Python完全入門
Effective Python