目次
はじめに
リスト内包表記でのラムダ式の使用中に、期待と違う動作をされて困ったので、備忘録として解決策を置いておこうと思います。考察もしていますが、厳密な説明ではなく、あくまで筆者の解釈です。
問題について
l = ['apple', 'grape', 'lemon']
l2 = [lambda:[i, id(i)] for i in l]
for j in l2:
print(j())
>>>
['lemon', 2409481603056]
['lemon', 2409481603056]
['lemon', 2409481603056]
このように、リスト内包表記の中でラムダ式を使うと、リストの要素が全て最後の要素と同じになってしまうので、これを解消する方法を紹介したいと思います。
解決方法
先に結論を言っておくと、以下のコードを
l2 = [lambda:[i, id(i)] for i in l]
このように書き換えると解決します。
l2 = [lambda x=i:[x, id(x)] for i in l]
i
を直接中身に渡すのではなく、一度x
に渡して、x
を中身に渡すという風に変わっています。こうすることで、関数を呼び出したときにi
の値が変わっていても関係なくなるため解決します。
追記:
コメントで教えていただいたのですが、以下のように変更しても解決可能です。
①
l2 = [(lambda i:
lambda : [i, id(i)]
)(i)
for i in l
]
引数を受け取った状態の関数を返すので、i
が書き換わっても関係がなくなるためです
②
l2 = (lambda:[i, id(i)] for i in l)
リスト内包表記の代わりにジェネレータ式を用いています。ジェネレータは次に値を取り出すまで処理を止めて待ってくれるので、i
が書き換わらずに待ってくれるためです。ただし、こちらは取り出す順番が決まっている場合にのみ有効な手段となります。
考察
ここからはなぜ上記の方法で解決できるのか考えてみたいと思います。
def test():
l = ['apple', 'grape', 'lemon']
l2 = []
for i in l:
func = lambda:[i, id(i)]
l2.append(func)
return l2
for j in test():
print(j())
>>>
['lemon', 1761796013040]
['lemon', 1761796013040]
['lemon', 1761796013040]
とりあえず、リスト内包表記を用いずに、この問題を再現してみました。再現できたという事は、リスト内包表記に特有な問題というわけではなさそうですね。これと同じことが、リスト内包表記でも起こっていると考えて、考察を行なっていきます。
まず、forループ中でのi
の扱いですが、配列から取り出した各要素とid
が一致します。これは変数の定義のように、普通の代入をしている状態です。つまり、上記のコードで言うなら、ループごとにi
というローカル変数を、上書きしている状態です。
次にfunc
について考えていきます。ここで重要になるのが、func
がどのタイミングでi
を受け取るかということです。上記のコードでは、リスト内包表記に見立てた関数を抜けた後で、外部からfunc
を呼び出しています。これは、test
の内部にローカル変数として定義されているfunc
を呼び出すと言う操作になります。どうやらfunc
がi
を参照するのが、定義された時ではなく、この呼び出された時のようなのです。実際return
の前にdel i
としてやると、値がないためエラーが出ますし、i = 'meron'
としてやれば、全て'meron'
に置き換わります。
要するに解決策としては、i
を受け取るのを、呼び出された時ではなく、定義された時にしてやれば良いわけです。lambda x=i:[x, id(x)]
としてやれば、いつ呼び出されても参照するのはx
の方になりますので、どれだけi
が上書きされようが、問題ないと言うわけですね。
リスト内包表記で実際に試してみると、期待通りの結果が得られました。
l = ['apple', 'grape', 'lemon']
l2 = [lambda x=i:[x, id(x)] for i in l
for j in l2:
print(j())
>>>
['apple', 2248410854192]
['grape', 2248413513840]
['lemon', 2248413513904]
結論
問題の原因は、ラムダ式が参照している先の変数が書き換わることだった。したがって、ラムダ式を使う際は、変数を直接渡すのを避けるべきである。リスト内包表記に限らず、関数内でラムダ式の定義を行う場合には、同様の問題が起こりうるので、注意が必要である。
最後に
今回は、リスト内包表記とラムダ式を組み合わせる際の注意点と、解決策を扱いました。自分はネットで検索してもうまくヒットせず、AIに聞いて解決した問題だったので、この記事が役に立ったと思う人が、一人でもいればいいなと思います。