結論
ではまず、 タグでオチが透けてる気がするので、 結論から見ていきましょう。
def func(x, y=2):
return x*2 + y
func(x=3) # => 8
は
# 関数定義
func = ((
ns['x']*2 + ns['y'] # x*2 + y に相当
if i else ns for ns in [{'y':2}] # x, y=2 に相当
for i in [0,1]) for _ in iter(list, None)) # おまじない
# 関数呼び出し
next(next(i) for i in [next(func)] if not next(i).update(x=3))
# => 8
とも書けます。これをジェネレーター式関数と言います。今、名付けました。
ご覧の通り、lambda式では無理な、デフォルト値の使用も出来ます。
とても便利そうですよね?
今回はこのジェネレーター式関数をしっかりと習得して、ライバルに差をつけてしまいましょう!
仕組み
この方法を理解するには以下のコードを理解する必要があります。
f = (x * 2 for _ in iter(list,None)) # 関数定義
x = 3
next(x) # 関数実行
x = 4
next(x) # 関数実行
一番最初に書いたコードは、これの細かな不具合を修正したものであり、関数定義も実行も、本質的には同じです。
というわけで細かく調べてみましょう。
ジェネレーターの理解
ジェネレーター式関数にはジェネレーター式、というものを使っています。リスト内包表記のジェネレーター版で、[]
ではなく()
で囲みます。
[i*3 for i in range(3)] # リスト内包表記
(i*3 for i in range(3)) # ジェネレーター式
上の2行を例えばfor文に食わせたときの挙動というのはだいたい同じなのですが、ジェネレーター式にはリスト内包表記にはない魅力があります。それは、定義時に計算を行わない、ということです。
li = [print("list") for _ in range(2)]
for _ in li:print("for")
print("---")
ge = (print("gene") for _ in range(2))
for _ in ge:print("for")
# list
# list
# for
# for
# ---
# gene
# for
# gene
# for
ご覧の通り、リスト内包表記はfor文が回る前に内部のprint("list")
が実行されているのに対して、ジェネレーター内包表記は、for文が回るたびに内部を実行していることが分かります。
これを応用すると、2つの面白いことが出来ます。
- 無限ジェネレーターを作る
- 未定義の変数を使用する
あと、世間の物好きの中には、メモリー削減や速度改善のために使う人もいるそうです。
それはともかくとして、1,2をそれぞれ見ていきましょう。そのあと、next()
を使ってジェネレーター式の実行結果を引き出す例も見てみます。
1.無限ジェネレーターを作る
pythonで無限ループをする方法はいくつかあります。
- 関数の再帰
- forのtargetをfor文内で増やす (詳細 : for for ∞ for python)
- iterの第2引数を使う
いずれもよく使う手法ですね。読者の皆さんの中にはwhile
やitertools.count
を使う、なんて方もいるかもしれません。
2,3の方法にジェネレーターを加えると、無限ジェネレーターを作ることが出来ます。
実際にやってみましょう!
# forのtargetの要素をfor文内で増やす
_4484p = ((len(il)-2)**2 for il in [[None]] for _ in il if not il.append(None))
for i in _4484p: print(i) # 無限ループ注意
# => 0, 1, 4, 9, ... , n**2, ...
# iterの第2引数を使う
itr2nd = (i ** 3 for i, _ in enumerate(iter(list, None)))
for i in itr2nd: print(i) # 無限ループ注意
# => 0, 1, 8, 27, ... , n**3, ...
解説は不要でしょう。
2.未定義の変数を使用する
ジェネレーター式を使うと
b = (x*i for i in range(3))
x = 3
list(b)
のようなコードが実行できます。これがb = [...]
のようなリスト内包表記ですとNameError: name 'x' is not defined
と怒られます。
だんだん関数っぽさが出てきましたね。
next
これは普通に役立つ知識なのがむしろ悔しいのですが、 ジェネレーターを呼び出すにはfor文を使うほかにもnext
を使います。
itr2nd = (i ** 3 for i, _ in enumerate(iter(list, None)))
print(next(itr2nd)) # => 0
print(next(itr2nd)) # => 1
print(next(itr2nd)) # => 8
print(next(itr2nd)) # => 27
のように、nextを複数回実行して、その挙動を理解してください。
あと、個人的なnext
の使いどころは、os.walk
です。
合わせ技
以上、「無限ループ」と「未定義変数の使用」で、
f = (x*2 for _ in iter(list,None)) # 関数定義
x = 3
print(next(x)) # => 6
x = 4
print(next(x)) # => 8
が理解できますね。これは、
- 繰り返し同じ処理を実行する
- 全く同じ処理ではなく、変数の値を変えるとそれに伴って処理も変わる
という点で「関数」なわけです。
問題点と改善
ここまで読んで、現在のジェネレーター式関数の問題にお気づきの読者もいると思います。
そうです。引数がありません。
引数がないため、スコープの問題が生じます。
f = (x for _ in iter(list, None))
x = "global"
print(next(f))
def local_scope():
x = "local"
print(next(f))
この関数を実行すると、実行した場所はlocal_scope
内なのに
local_scope() # => global
のように、globalを(厳密にいうと、ジェネレーター式関数を定義したスコープ)参照してしますのです。
要するに現段階のジェネレーター式関数は
def f():
return x
と同じ挙動になってしまっているのです。
これを改善するための改造が
# 関数定義
func = ((
ns['x']*2 + ns['y'] # x*2 + y に相当
if i else ns for ns in [{'y':2}] # x, y=2 に相当
for i in [0,1]) for _ in iter(list, None)) # おまじない
# 関数呼び出し
next(next(i) for i in [next(func)] if not next(i).update(x=3))
だったわけです。
この改善点の骨子は以下のとおりです。
- ジェネレーター式関数定義時に、ジェネレーター式を二重にする
- 外側のジェネレーターは内側のジェネレーターを無限に返す
- 内側のジェネレーターが処理の実体
- 内側のジェネレーター式は名前空間として辞書を使用(
ns
) - 呼び出し時、内側のジェネレーター(
i
)に対してnext(i)
を2回実行。1回目はns
を、2回目は実行結果を返す - 引数の受け渡しを、
ns.update
で実現
nsについて、もっと知りたい方はリスト内包表記で始める超"実用的"なPythonワンライナー入門を参照してください。
まとめ
いかがでしたか?
今回は、ジェネレーター式を使って関数を作る方法を紹介しました。
キーワード引数しか渡せない、といった欠点はありますが、それを上回る魅力のある関数でしたね。
みなさんもぜひ、ジェネレーター式関数を使って周囲をあっと言わせましょう!