ループで関数を作成する


102

ループ内に関数を作成しようとしています:

functions = []

for i in range(3):
    def f():
        return i

    # alternatively: f = lambda: i

    functions.append(f)

問題は、すべての機能が同じになることです。0、1、2を返す代わりに、3つの関数はすべて2を返します。

print([f() for f in functions])
# expected output: [0, 1, 2]
# actual output:   [2, 2, 2]

なぜこれが起こっているのですか?また、それぞれ0、1、2を出力する3つの異なる関数を取得するにはどうすればよいですか?


4
:自分へのリマインダーとしてdocs.python-guide.org/en/latest/writing/gotchas/...
Chuntao呂

回答:


167

遅延バインディングで問題が発生しています。各関数はi可能な限り遅く検索します(したがって、ループの終了後に呼び出されると、iに設定されます2)。

変更を:簡単に初期の結合強制することで固定def f():def f(i=i):、このように:

def f(i=i):
    return i

デフォルト値(右側ii=i引数名のデフォルト値であるi左で、i中にはi=i)を見上げているdefではないで、時間callの時間なので、基本的に彼らは、特に初期の結合を探しへの道です。

f追加の引数を取得することを心配している場合(したがって、誤って呼び出される可能性がある場合)、クロージャーを「関数ファクトリー」として使用することを含む、より洗練された方法があります。

def make_f(i):
    def f():
        return i
    return f

ループf = make_f(i)では、defステートメントの代わりに使用します。


7
これらの問題を修正する方法をどのようにして知っていますか?
alwbtc

3
@alwbtcそれはほとんど単なる経験であり、ほとんどの人はいつか自分でこれらの問題に直面しています。
ruohola

なぜ機能しているのか説明してもらえますか?(あなたはループで生成されたコールバックで私を救います、引数は
常に

20

説明

ここでの問題はi、関数のf作成時にの値が保存されないことです。むしろ、それが呼び出されたときfの値を調べます。i

考えてみれば、この動作は完全に理にかなっています。実際、これは関数が機能するための唯一の合理的な方法です。次のように、グローバル変数にアクセスする関数があるとします。

global_var = 'foo'

def my_function():
    print(global_var)

global_var = 'bar'
my_function()

このコードを読んだとき、当然のことながら、「foo」ではなく「bar」が出力されることを期待global_varします。関数の宣言後にの値が変更されたためです。同じことがあなた自身のコードでも起こっています:あなたがを呼び出すfまでに、の値はiに変更され、に設定されてい2ます。

ソリューション

この問題を解決するには多くの方法があります。ここにいくつかのオプションがあります:

  • iデフォルト引数として使用することにより、事前バインディングを強制します

    (のようなi)クロージャー変数とは異なり、デフォルトの引数は、関数が定義されるとすぐに評価されます。

    for i in range(3):
        def f(i=i):  # <- right here is the important bit
            return i
    
        functions.append(f)

    これがどのように/なぜ機能するかについて少し洞察を与えるために:関数のデフォルト引数は関数の属性として保存されます。したがって、の現在のiがスナップショットで保存されます。

    >>> i = 0
    >>> def f(i=i):
    ...     pass
    >>> f.__defaults__  # this is where the current value of i is stored
    (0,)
    >>> # assigning a new value to i has no effect on the function's default arguments
    >>> i = 5
    >>> f.__defaults__
    (0,)
  • 関数ファクトリーを使用iして、クロージャー内の現在の値を取得します。

    あなたの問題の根本iは、それが変化する可能性がある変数であるということです。変更されないことが保証されている別の変数を作成することで、この問題を回避できます。これを行う最も簡単な方法は、クロージャーです。

    def f_factory(i):
        def f():
            return i  # i is now a *local* variable of f_factory and can't ever change
        return f
    
    for i in range(3):           
        f = f_factory(i)
        functions.append(f)
  • 使用functools.partialの現在の値バインドするiにはf

    functools.partial既存の関数に引数をアタッチできます。ある意味では、それも一種の関数ファクトリーです。

    import functools
    
    def f(i):
        return i
    
    for i in range(3):    
        f_with_i = functools.partial(f, i)  # important: use a different variable than "f"
        functions.append(f_with_i)

警告:これらのソリューションは、変数に新しい値を割り当てる場合にのみ機能します。変数に格納されているオブジェクトを変更すると、同じ問題が再び発生します。

>>> i = []  # instead of an int, i is now a *mutable* object
>>> def f(i=i):
...     print('i =', i)
...
>>> i.append(5)  # instead of *assigning* a new value to i, we're *mutating* it
>>> f()
i = [5]

iデフォルトの引数に変えたにもかかわらず、いかに変更されたかに注意してください!コードが変化 する場合は、次のようにのコピーを関数にiバインドする必要がありますi

  • def f(i=i.copy()):
  • f = f_factory(i.copy())
  • f_with_i = functools.partial(f, i.copy())
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.