Pythonのリスト内包表記におけるlambda関数の落とし穴

1、問題

以下のコードの実行結果とその理由を説明してください。

func_list = [lambda num: num + j for j in range(10)]
print(func_list[3](5))

2、解答

このコードを実行すると、出力は14となります。なぜすべてのlambda関数が同じ値を返すのでしょうか?

この現象を理解するには、いくつかのPythonの概念を確認する必要があります。

2.1、リスト内包表記

リスト内包表記は、特定の構文を使ってリストを生成するための機能です。

構文形式:

[expression for iterable_var in iterable]

動作プロセス:

  • iterableの各要素を反復処理します。
  • 反復した値をiterable_varに代入し、expressionを評価して新しい値を計算します。
  • すべてのexpressionの結果を新しいリストとして返します。

動作は以下のforループと同等です:

L = []

for j in range(10):
    # 例えば、expressionがiterable_var * 5の場合、この結果をリストに追加します
    L.append(j * 5)

この点を理解した上で、元の問題のコードを見てみましょう。

func_list = [lambda num: num + j for j in range(10)]
# 後のlambda num: num + jがexpressionです。これは通常のlambda式ですが、生成されるのは関数のリストです。
# 以下の例を見るとより分かりやすいでしょう。
L = []

for j in range(10):
    L.append(lambda num: num + j)

では、func_list[3](5)を実行すると何が起こるのでしょうか?これはlambda 5: 5 + jを実行することに相当します。しかし、なぜjは常に9になるのでしょうか?

2.2、関数内部変数の後期バインディング

関数を定義する際、関数内では変数の値を保存せず、実行時にその値がどこでバインドされているかを探します。

例えば、関数内で変数を参照する場合、事前に定義する必要はありません。使用前に定義すればよいのです。以下のコードを参照してください。

def outer_func():
    print(inner_var)

inner_var = "hello world"

outer_func()

注: 関数を呼び出す際、その関数は事前に定義されている必要があります。これを「前方参照」と呼びます。一方、関数内部の変数は後から定義されても問題ありません。これを「後期バインディング」と呼びます。

この変数jはlambda内で定義されたものではなく、リスト内包表記内で定義されたものです。リスト内包表記をforループに変換してみましょう。

def create_func_list():
    L = []
    for j in range(10):
        def inner_func(arg):
            return j + arg
        L.append(inner_func)
    return L

func_list = create_func_list()
print(func_list[3](5))

上記のコードでは、最終的に返されるのは関数のリストです。そして、このjの値はループの際に代入されます。inner_func(lambda)内ではjは定義されていませんが、外側のスコープの変数を参照しています。inner_funcを呼び出す時点で、このforループはすでに終了しており、jの値は固定値である9になっています。

そのため、lambda num: num + jを呼び出すたびに、このjは常に9になります。

2.3、簡単な修正

もし、ループの各イテレーションでjの値が異なるようにしたい場合はどうすればよいでしょうか?外側の変数を直接参照するのではなく、変数を引数として渡せば解決します。

func_list = [lambda num, j=j: num + j for j in range(10)]
print(func_list[3](5))

これをforループで表現すると以下のようになります。

def create_func_list():
    L = []
    for j in range(10):
        def inner_func(arg, j=j):
            return j + arg
        L.append(inner_func)
    return L

func_list = create_func_list()
print(func_list[3](5))

2.4、クロージャ

簡単に説明すると、ネストされた関数が、外側の関数の変数(グローバル変数ではない)を参照することをクロージャと呼びます。

def outer():
    x = 1

    def inner():
        print(x)

outerを実行するとクロージャが生成されます。outerの実行が終了しても、内側の関数innerxを使用しているため、変数xはガベージコレクションされません。一般的な応用例としてデコレータがあります。

タグ: Python リスト内包表記 ラムダ式 クロージャ 後期バインディング

6月12日 21:25 投稿