Pythonでパラメータから関数作成とか

Pythonに関して、少しメモ。Pythonのバージョンは3.10.1

func_list = [(lambda v: v * i) for i in range(3)]
print([f(5) for f in func_list])
# -> [10, 10, 10]

1行目で、やろうとしていることはわかるだろう。何かデータの集合があって、そのデータを用いるような関数を複数生成しておいて、リストに格納しておきたい、ということがある。そういうことをしようとしている。

両辺の変数の数が異なるラムダ式なので違和感はある。スコープが怪しい気もする。

実際、2行目で使ってみると、期待通りに動作しない。

これは、ラムダ式が「定義された段階で」変数iが展開されないためであろうと考えられる。

for文で試してみる。

func_list = []

for i in range(3):
    f = (lambda v: v * i)
    func_list.append(f)

print([f(5) for f in func_list])
# -> [10, 10, 10]

やはり全てが10になった。 これがどうなっているのか調べる。

func_list = []

for i in range(3):
    f = (lambda v: v * i)
    func_list.append(f)

print([id(f) for f in func_list])
# -> [139918026063248, 139918025554336, 139918025554480]

idは全て異なる値になった。したがって、3つのラムダ式は別のメモリ領域に格納されていると考えられる。 しかし実際には同じ値のiを乗算するようになってしまっている。

func_list = []

for i in range(3):
    print([_f(5) for _f in func_list])
    f = (lambda v: v * i)
    func_list.append(f)
    print([_f(5) for _f in func_list])

# --- OUTPUT ---
# []
# [0]
# [5]
# [5, 5]
# [10, 10]
# [10, 10, 10]

ループ用の変数iが新しくなった時点でラムダ式の定義が書き換わっている。 やはり、ラムダ式は変数iへの参照を持っているようだ。

def make_func(i):
    my_func = (lambda v: v * i)
    return my_func

func_list = []

for i in range(3):
    func_list.append(make_func(i))

print([f(5) for f in func_list])

# -> [0, 5, 10]

こうすると、うまくいく(関数の中のラムダ式の部分は、関数で書いても機能する)。 これは結構奇妙な結果に思う。関数に渡すときに変数iへの紐づけが切れるのだろうか。

func_list = []

for i in range(3):
    conv = (lambda _i: (lambda v: v * _i))
    func_list.append(conv(i))

print([f(5) for f in func_list])

ひょっとして、と思い、「関数を返す関数」の形のラムダ式を作ってみたところ、うまく機能した。

参照する際のスコープの広さに頼って、うっかり最初の形で書いてしまいがちであるが、 パラメータから、「パラメータを用いる関数」を作る時は、必ず「関数を返す関数」の形にした方が良いのかもしれない。

class Converter:
    def __init__(self, coeff):
        self.coeff = coeff

    def __call__(self, v):
        return v * self.coeff


func_list = [Converter(i) for i in range(3)]

print([f(5) for f in func_list])

個人的には、ラムダ式はたまに使う程度で、不安が残る。このようにクラスで書く方が良いかもしれない。

追記(2022/5/17)

https://docs.python-guide.org/writing/gotchas/

このページに記載があった。

Python はlate bindingと言って、定義時ではなく実行時に変数が展開されるためにうまくいかないようだ。 しかし現実問題として、定義時に部分的に変数を展開するように処理を書きたい場合もある。PostScriptの//のような手早い記法があると嬉しい。Pythonでは、解決策として、デフォルト引数を用いる方法があるとのこと。

func_list = [(lambda v, _i=i: v * _i) for i in range(3)]
print([f(5) for f in func_list])
# -> [0, 5, 10]

実際、こうしてみるとうまく機能した。慣れの問題かもしれないが、なんとも気持ち悪い。実践時もうっかりデフォルト引数を挟むのを忘れてしまいそうである。