ジェネレータ
ここで、ジェネレータの表現やジェネレータの関数を含むPythonのジェネレータに深く潜ってみましょう。a
ジェネレータの表現
リストの内包表現とジェネレータの内包表現の間の違いは、ときどき困惑させるものです。ここでは、さっとそれらの間の違いをだしてみましょう。
ジェネレータの表現が括弧を使う一方で、リスト内包表現は大括弧を使います
これはリストの内包表現を表します。
In [1]:
[n ** 2 for n in range(12)]
Out[1]:
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121]
一方で、これはジェネレータの表現です。
In [2]:
(n ** 2 for n in range(12))
Out[2]:
<generator object <genexpr> at 0x104a60518>
ジェネレータの表現を表示することは中身を表示しないことに留意してください。ジェネレータの表現の中身を表示する一つの方法は、リストコンストラクタに通過させることです。
In [3]:
G = (n ** 2 for n in range(12))
list(G)
Out[3]:
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121]
ジェネレータが値を生み出すレシピである一方で、リストは値のコレクションです
リストを作るとき、実際に値の集まりを作っています、そしてそれについてのいくつかのメモリコストがあります。ジェネレータを作るとき、あなたは、値の集まりではなくそれらの値を生み出すレシピを作ります。両方とも同じイテレータインタフェースを表します、このようにみれます:
In [4]:
L = [n ** 2 for n in range(12)]
for val in L:
print(val, end=' ')
0 1 4 9 16 25 36 49 64 81 100 121
In [5]:
G = (n ** 2 for n in range(12))
for val in G:
print(val, end=' ')
0 1 4 9 16 25 36 49 64 81 100 121
ジェネレータの表現は実際にはそれらが必要とされるまで、値を計算しないのです。これはメモリ効果だけでなく同じように計算の効率に導きます。これはまたリストのサイズが利用可能なメモリによって制限される一方で、ジェネレータの表現のサイズが制限されないことを意味します。
無限のジェネレータ表現の例として、itertoolsで定義されるcountイテレータを使うことを作ります:
from itertools import count
count()
Out[6]:
count(0)
In [7]:
for i in count():
print(i, end=' ')
if i >= 10: break
0 1 2 3 4 5 6 7 8 9 10
countイテレータは幸運にもあなたが止めることを伝えるまで数えつづけます。これは、また永遠に続くジェネレータをつくるのに、便利です:
In [8]:
factors = [2, 3, 5, 7]
G = (i for i in count() if all(i % n > 0 for n in factors))
for val in G:
print(val, end=' ')
if val > 40: break
1 11 13 17 19 23 29 31 37 41
ここで得られたものをみるかもしれません:もしfactorsのlistを適当に拡張するなら、私たちがはじめにもてるものは、Sieve of Eratosthenes アルゴリズムを使った素数ジェネレータです。より瞬間的にこれを探求しましょう。
複数回反復されるリスト:ジェネレータ表現は一度使いです
これはジェネレータ表現の潜在的な理解の一つです。リストで、そのままやってみましょう。
In [9]:
L = [n ** 2 for n in range(12)]
for val in L:
print(val, end=' ')
print()
for val in L:
print(val, end=' ')
0 1 4 9 16 25 36 49 64 81 100 121
0 1 4 9 16 25 36 49 64 81 100 121
他方で、ジェネレータ表現は一つの反復後に使われます。
In [10]:
G = (n ** 2 for n in range(12))
list(G)
Out[10]:
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121]
In [11]:
list(G)
Out[11]:
[]
これは反復をとめて開始することを意味するのでとても使いやすいです。
This can be very useful because it means iteration can be stopped and started:
In [12]:
G = (n**2 for n in range(12))
for n in G:
print(n, end=' ')
if n > 30: break
print("\ndoing something in between")
for n in G:
print(n, end=' ')
0 1 4 9 16 25 36
doing something in between
49 64 81 100 121
これが役に立つとわかる一つの場所はディスク上のデータファイルの集まりで動作するときです。それはバッチの中でそれらをかなり分析しやすくし、ジェネレータがまだみてないものを追跡し続けるようにします。
ジェネレータ関数:yieldを使うこと
前の節でリストの内包表現は、通常のforループを使うことがより複雑な状況でよりよくなる一方で、単純なリストを作るのと比較して最高ということをみました。同じことがジェネレータ表現でも正しいです:ジェネレータ関数を使って、より複雑なジェネレータを作りましょう、ジェネレータ関数はyield文の利用を作ります。
ここで、同じリストを構成する二つの方法をもちましょう:
In [13]:
L1 = [n ** 2 for n in range(12)]
L2 = []
for n in range(12):
L2.append(n ** 2)
print(L1)
print(L2)
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121]
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121]
よくにてて、ここでは同じジェネレータを作る二つの方法をもちましょう:
In [14]:
G1 = (n ** 2 for n in range(12))
def gen():
for n in range(12):
yield n ** 2
G2 = gen()
print(*G1)
print(*G2)
0 1 4 9 16 25 36 49 64 81 100 121
0 1 4 9 16 25 36 49 64 81 100 121
ジェネレータ関数は、一つの値を一度返すために使うreturnよりむしろ、値のシーケンス(潜在的には無限の)を産出するためにyieldを使う、という機能です。
ちょうどジェネレータ表現のように、ジェネレータの状態が部分的な反復の間で予約されますが、ジェネレータの新しいコピーをほしいなら、単に再度関数を呼び出せます。
例:素数生成
ここで、私のジェネレータの関数のお気に入りの例を示します:素数の不規則な連続を生み出す機能です。これの古いアルゴリズムは、このように動くSieve of Eratostheseです:
In [15]:
# Generate a list of candidates
L = [n for n in range(2, 40)]
print(L)
[2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39]
In [16]:
# Remove all multiples of the first value
L = [n for n in L if n == L[0] or n % L[0] > 0]
print(L)
[2, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33, 35, 37, 39]
In [17]:
# Remove all multiples of the second value
L = [n for n in L if n == L[1] or n % L[1] > 0]
print(L)
[2, 3, 5, 7, 11, 13, 17, 19, 23, 25, 29, 31, 35, 37]
In [18]:
# Remove all multiples of the third value
L = [n for n in L if n == L[2] or n % L[2] > 0]
print(L)
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37]
もしこの手続きを、十分大きいリストで、十分な回数繰り返すと、のぞみのように多くの素数を生成できます。
この論理をジェネレータ関数の中にいれてみましょう:
In [19]:
def gen_primes(N):
"""Generate primes up to N"""
primes = set()
for n in range(2, N):
if all(n % p > 0 for p in primes):
primes.add(n)
yield n
print(*gen_primes(100))
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97
以上がitについてのものです。これはSieve of Eratosthenesの計算的にたいていの効果的な実装ではない一方で、ジェネレータ関数のシンタックスがより複雑なシーケンスを組み立てるのに使われるのでいかに便利かを表します。