遅延バインディング (late binding)
2024/4/9
Python
目次
問題のコード
コード例1
まず、問題のコードを見てみましょう。
>>> my_ld = [lambda x: x * i for i in range(3)]
>>> my_list = [ld(2) for ld in my_ld]
>>> my_list
[4, 4, 4]
>>>
コード例2
>>> def my_func(x, a=1):
... return x * a
...
>>> my_func(1,3)
3
>>> my_func(5,3)
15
>>> my_ld = [lambda x: my_func(x, i) for i in range(3)]
>>> my_list = [ld(2) for ld in my_ld]
>>> my_list
[4, 4, 4]
-
lambda x: x * i:つまり、$x^i$という意味。 -
ld(2):つまり、$2 \times i$という意味、iは[0,1,2]です。 -
見込む結果は[0, 2, 4]ですが、実際の結果は[4, 4, 4]でした。
このコードでは、リスト内包表記の中で lambda x: x * i を定義していますが、各 lambda は変数 [i] の「現在の値」ではなく、「参照(アドレス)」を保持しています。
ループが終了した時点で [i] は 2 になっているため、すべての lambda 関数が x * 2 を実行することになり、結果として [4, 4, 4] になります。
解決方法1:デフォルト引数を使う
Python の関数や lambda のデフォルト引数は定義された時点で評価されるため、これを使って「現在の値を固定」することができます。
コード例1
>>> my_ld = [lambda x, a=i: x * a for i in range(3)]
>>> my_list = [ld(2) for ld in my_ld]
>>> my_list
[0, 2, 4]
コード例2
>>> my_ld = [lambda x, a=i: my_func(x, a) for i in range(3)]
>>> my_list = [ld(2) for ld in my_ld]
>>> my_list
[0, 2, 4]
a=iというデフォルト引数により、ループごとにiの値が固定されます。- 各
lambdaはそれぞれa=0,a=1,a=2を持つようになるので、期待通りの結果を得られます。
解決方法2:paritial を使う
functools.partial は Python 標準ライブラリの functools モジュールに含まれる関数で、関数の一部の引数を固定して新しい関数を作成するための機能です。
コード例1
>>> my_ld = [partial(lambda x, a: x * a, a=i) for i in range(3)]
>>> my_list = [ld(2) for ld in my_ld]
>>> my_list
[0, 2, 4]
コード例2
>>> my_ld = [partial(my_func, a=i) for i in range(3)]
>>> my_list = [ld(2) for ld in my_ld]
>>> my_list
結論
- 遅延バインディングは Python 特有の挙動であり、特にループ内でクロージャや
lambdaを生成するときに注意が必要です。 - デフォルト引数や
functools.partialを使い、意図した値を確実にキャプチャしましょう。 - 可読性・保守性を考えると、
functools.partialの使用も検討してください。