Bir döngüde işlevler oluşturma


107

Bir döngünün içinde işlevler oluşturmaya çalışıyorum:

functions = []

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

    # alternatively: f = lambda: i

    functions.append(f)

Sorun şu ki, tüm işlevler aynı hale geliyor. 0, 1 ve 2'yi döndürmek yerine, üç işlevin tümü 2'yi döndürür:

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

Bu neden oluyor ve sırasıyla 0, 1 ve 2 çıktı veren 3 farklı işlevi elde etmek için ne yapmalıyım?


Yanıtlar:


172

Geç bağlamayla ilgili bir sorunla karşılaşıyorsunuz - her işlev iolabildiğince geç arar (bu nedenle, döngünün bitiminden sonra çağrıldığında i, olarak ayarlanır 2).

Erken bağlamayı zorlayarak kolayca düzeltildi: def f():aşağıdaki def f(i=i):gibi değiştirin :

def f(i=i):
    return i

Varsayılan değerler (sağ iiçinde i=iargüman adı için bir varsayılan değerdir isol olduğunu iiçinde i=i) baktı edilir defdeğil, zaman callzaman, bu nedenle esasen onlar özellikle erken bağlama arayan bir yolunu konum.

fFazladan bir argüman almaktan (ve dolayısıyla potansiyel olarak yanlışlıkla çağrılmaktan) endişeleniyorsanız , bir "işlev fabrikası" olarak bir kapatmayı kullanmanın daha karmaşık bir yolu vardır:

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

ve döngünüzde ifade f = make_f(i)yerine kullanın def.


7
bunları nasıl düzelteceğini nasıl biliyorsun?
alwbtc

3
@alwbtc çoğunlukla sadece deneyimdir, çoğu insan bir noktada bu şeylerle kendi başına yüzleşmiştir.
ruohola

Neden işe yaradığını açıklar mısınız lütfen? (Beni döngüde oluşturulan geri
aramadan kurtardınız

23

Açıklama

Buradaki sorun i, işlev foluşturulduğunda değerinin kaydedilmemesidir . Aksine, fdeğerini arar io zaman denir .

Düşünürseniz, bu davranış çok mantıklı. Aslında, işlevlerin çalışmasının tek mantıklı yolu budur. Küresel bir değişkene erişen bir işleve sahip olduğunuzu düşünün, örneğin:

global_var = 'foo'

def my_function():
    print(global_var)

global_var = 'bar'
my_function()

Bu kodu okuduğunuzda, tabii ki - "foo" değil "bar" yazdırmasını beklersiniz, çünkü global_varişlev bildirildikten sonra değerinin değeri değişmiştir. Aynı şey kendi kodunuzda da oluyor: Aradığınızda fdeğeri ideğişmiş ve olarak ayarlanmıştır 2.

Çözüm

Aslında bu sorunu çözmenin birçok yolu var. İşte birkaç seçenek:

  • iVarsayılan bağımsız değişken olarak kullanarak erken bağlamayı zorla

    Kapanış değişkenlerinden (gibi i) farklı olarak , varsayılan argümanlar işlev tanımlandığında hemen değerlendirilir:

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

    Bunun nasıl / neden çalıştığına dair biraz fikir vermek gerekirse: Bir işlevin varsayılan argümanları işlevin bir niteliği olarak saklanır; böylece geçerli değeri ianlık olarak alınır ve kaydedilir.

    >>> 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,)
    
  • iBir kapanışta mevcut değerini yakalamak için bir işlev fabrikası kullanın

    Sorununuzun kökü i, değişebilen bir değişkendir. Asla değişmemesi garanti edilen başka bir değişken oluşturarak bu sorunu aşabiliriz - ve bunu yapmanın en kolay yolu kapanıştır :

    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)
    
  • Kullanım functools.partialmevcut değerini bağlamak iiçinf

    functools.partialmevcut bir işleve argümanlar eklemenizi sağlar. Bir bakıma o da bir tür işlev fabrikası.

    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)
    

Uyarı: Bu çözümler sadece iş eğer atamak değişkene yeni bir değer. Eğer varsa değiştirmek değişkeninde saklanan nesneyi, yine aynı sorunla karşılaştığınızda edeceğiz:

>>> 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]

iVarsayılan bir argüman haline getirmemize rağmen hala nasıl değiştiğine dikkat edin ! Kodunuz Eğer mutasyon i , o zaman bir bağlama gerekir kopyasını ait ifonksiyonunuza yüzden mi:

  • def f(i=i.copy()):
  • f = f_factory(i.copy())
  • f_with_i = functools.partial(f, i.copy())
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.