【上級者も知らない！？】Pythonで関数を作る第3の方法【lambda式より便利？！】

# 結論
ではまず、 ~~タグでオチが透けてる気がするので、~~ 結論から見ていきましょう。

```python
def func(x, y=2):
    return x*2 + y

func(x=3) # => 8
```

は

```python
# 関数定義
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式では無理な、デフォルト値の使用も出来ます。
とても便利そうですよね？

今回はこのジェネレーター式関数をしっかりと習得して、ライバルに差をつけてしまいましょう！

# 仕組み
この方法を理解するには以下のコードを理解する必要があります。

```python
f = (x * 2 for _ in iter(list,None)) # 関数定義
x = 3
next(x) # 関数実行
x = 4
next(x) # 関数実行
```

一番最初に書いたコードは、これの細かな不具合を修正したものであり、関数定義も実行も、本質的には同じです。
というわけで細かく調べてみましょう。

## ジェネレーターの理解
ジェネレーター式関数にはジェネレーター式、というものを使っています。リスト内包表記のジェネレーター版で、`[]`ではなく`()`で囲みます。

```python
[i*3 for i in range(3)] # リスト内包表記
(i*3 for i in range(3)) # ジェネレーター式
```

上の2行を例えばfor文に食わせたときの挙動というのはだいたい同じなのですが、ジェネレーター式にはリスト内包表記にはない魅力があります。それは、定義時に計算を行わない、ということです。

```python
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. 未定義の変数を使用する

あと、世間の物好きの中には、メモリー削減や速度改善のために使う人もいるそうです。
それはともかくとして、1,2をそれぞれ見ていきましょう。そのあと、`next()`を使ってジェネレーター式の実行結果を引き出す例も見てみます。

### 1.無限ジェネレーターを作る
pythonで無限ループをする方法はいくつかあります。

1. 関数の再帰
2. forのtargetをfor文内で増やす （詳細 : [for for ∞ for python](https://qiita.com/KTakahiro1729/items/d58b93b8a41f939a0fad)）
3. iterの第2引数を使う

いずれもよく使う手法ですね。読者の皆さんの中には`while`や`itertools.count`を使う、なんて方もいるかもしれません。
2,3の方法にジェネレーターを加えると、無限ジェネレーターを作ることが出来ます。
実際にやってみましょう！

```python
# 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.未定義の変数を使用する
ジェネレーター式を使うと

```python
b = (x*i for i in range(3))
x = 3
list(b)
```

のようなコードが実行できます。これが`b = [...]`のようなリスト内包表記ですと`NameError: name 'x' is not defined`と怒られます。

だんだん関数っぽさが出てきましたね。

### next
~~これは普通に役立つ知識なのがむしろ悔しいのですが、~~ ジェネレーターを呼び出すにはfor文を使うほかにも`next`を使います。

```python
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`です。


## 合わせ技
以上、「無限ループ」と「未定義変数の使用」で、

```python
f = (x*2 for _ in iter(list,None)) # 関数定義
x = 3
print(next(x)) # => 6
x = 4
print(next(x)) # => 8
```

が理解できますね。これは、

- 繰り返し同じ処理を実行する
- 全く同じ処理ではなく、変数の値を変えるとそれに伴って処理も変わる

という点で「関数」なわけです。

## 問題点と改善
ここまで読んで、現在のジェネレーター式関数の問題にお気づきの読者もいると思います。
そうです。引数がありません。

引数がないため、スコープの問題が生じます。

```python
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を（厳密にいうと、ジェネレーター式関数を定義したスコープ）参照してしますのです。

要するに現段階のジェネレーター式関数は

```python
def f():
  return x
```

と同じ挙動になってしまっているのです。

これを改善するための改造が

```python
# 関数定義
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))
```

だったわけです。
この改善点の骨子は以下のとおりです。

1. ジェネレーター式関数定義時に、ジェネレーター式を二重にする
2. 外側のジェネレーターは内側のジェネレーターを無限に返す
3. 内側のジェネレーターが処理の実体
4. 内側のジェネレーター式は名前空間として辞書を使用(`ns`)
5. 呼び出し時、内側のジェネレーター(`i`)に対して`next(i)`を2回実行。1回目は`ns`を、2回目は実行結果を返す
6. 引数の受け渡しを、`ns.update`で実現

nsについて、もっと知りたい方は[リスト内包表記で始める超"実用的"なPythonワンライナー入門](https://qiita.com/KTakahiro1729/items/2abef90fee3ec7b31e3e)を参照してください。

# まとめ
いかがでしたか？
今回は、ジェネレーター式を使って関数を作る方法を紹介しました。
キーワード引数しか渡せない、といった欠点はありますが、それを上回る魅力のある関数でしたね。

みなさんもぜひ、ジェネレーター式関数を使って周囲をあっと言わせましょう！