LoginSignup
20
18

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-08-08

結論

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

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

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

1.無限ジェネレーターを作る

pythonで無限ループをする方法はいくつかあります。

  1. 関数の再帰
  2. forのtargetをfor文内で増やす (詳細 : for for ∞ for python
  3. iterの第2引数を使う

いずれもよく使う手法ですね。読者の皆さんの中にはwhileitertools.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))

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

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

nsについて、もっと知りたい方はリスト内包表記で始める超"実用的"なPythonワンライナー入門を参照してください。

まとめ

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

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

20
18
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
18