2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【リスト内包表記】ラムダ式と組み合わせたら困った話

Last updated at Posted at 2024-11-03

目次

はじめに

 リスト内包表記でのラムダ式の使用中に、期待と違う動作をされて困ったので、備忘録として解決策を置いておこうと思います。考察もしていますが、厳密な説明ではなく、あくまで筆者の解釈です。

問題について

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が書き換わらずに待ってくれるためです。ただし、こちらは取り出す順番が決まっている場合にのみ有効な手段となります。

考察

 ここからはなぜ上記の方法で解決できるのか考えてみたいと思います。

test.py
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を呼び出すと言う操作になります。どうやらfunciを参照するのが、定義された時ではなく、この呼び出された時のようなのです。実際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に聞いて解決した問題だったので、この記事が役に立ったと思う人が、一人でもいればいいなと思います。

2
1
6

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
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?