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の実行が終了しても、内側の関数innerがxを使用しているため、変数xはガベージコレクションされません。一般的な応用例としてデコレータがあります。