(Lambda) fonksiyon kapakları ne yakalar?


249

Son zamanlarda Python ile oynamaya başladım ve kapakların çalışma biçimine özgü bir şey ortaya çıktı. Aşağıdaki kodu göz önünde bulundurun:

adders=[0,1,2,3]

for i in [0,1,2,3]:
   adders[i]=lambda a: i+a

print adders[1](3)

Tek bir girdi alan ve bir sayı tarafından eklenen girdiyi döndüren basit bir işlev dizisi oluşturur. İşlevler for, yineleyicinin ila iarasında çalıştığı döngüde 0oluşturulur 3. Bu sayıların her biri için onu lambdayakalayan ive işlevin girişine ekleyen bir işlev oluşturulur . Son satır ikinci lambdafonksiyonu 3parametre olarak çağırır . Benim için sürpriz çıktı 6.

Beklediğim bir 4. Benim mantığım şuydu: Python'da her şey bir nesnedir ve bu nedenle her değişken onun için bir göstergedir. İçin lambdakapanışları oluştururken i, şu anda işaret ettiği tamsayı nesnesine bir işaretçi saklamasını bekledim i. Bu, iyeni bir tamsayı nesnesi atandığında, önceden oluşturulan kapanmaları etkilememesi gerektiği anlamına gelir . Ne yazık ki, addersdiziyi bir hata ayıklayıcı içinde incelemek, bunu yaptığını gösterir. Tüm lambdafonksiyonlar son değerine bakın i, 3içinde, hangi sonuçların adders[1](3)dönen 6.

Hangi beni merak ediyorsun:

  • Kapaklar tam olarak ne yakalar?
  • lambdaİşlevleri , değerini değiştirdiğinde ietkilenmeyecek şekilde , geçerli değerini yakalamaya ikna etmenin en zarif yolu nedir i?

35
UI kodunda bu sorunu yaşadım. Beni deli etti. İşin püf noktası, döngülerin yeni bir kapsam oluşturmadığını hatırlamaktır.
Haziran'da

3
@TimMB Ad alanından nasıl çıkılır i?
13'te

3
@detly Peki print ibunun döngüden sonra işe yaramayacağını söyleyecektim . Ama bunu kendim için test ettim ve şimdi ne demek istediğini anlıyorum - işe yarıyor. Ben döngü değişkenleri python döngü gövdeden sonra kalan hiçbir fikrim yoktu.
Tim MB

1
@TimMB - Evet, demek istediğim buydu. İçin aynı if, with, tryvb
detly

13
Bu, resmi Python SSS bölümünde, Neden farklı değerlere sahip bir döngüde tanımlanan lambdasların hepsi aynı sonucu döndürüyor? hem bir açıklama hem de olağan bir çözümle.
abarnert

Yanıtlar:


161

İkinci sorunuz cevaplandı, ancak ilk soruya gelince:

kapak tam olarak ne yakalar?

Python'da kapsam belirleme dinamik ve sözcükseldir. Bir kapatma, işaret ettiği nesneyi değil, değişkenin adını ve kapsamını her zaman hatırlar. Örneğinizdeki tüm işlevler aynı kapsamda oluşturulduğundan ve aynı değişken adını kullandığından, her zaman aynı değişkene başvururlar.

EDIT: Bunun üstesinden gelmekle ilgili diğer sorunuzla ilgili olarak, akla gelen iki yol vardır:

  1. En özlü, ama kesinlikle eşdeğer olmayan yol Adrien Plisson tarafından önerilen yöntemdir . Fazladan bağımsız değişken içeren bir lambda oluşturun ve fazladan bağımsız değişkenin varsayılan değerini korunmasını istediğiniz nesneye ayarlayın.

  2. Lambda'yı her oluşturduğunuzda yeni bir kapsam oluşturmak için biraz daha ayrıntılı ama daha az hileli olurdu:

    >>> adders = [0,1,2,3]
    >>> for i in [0,1,2,3]:
    ...     adders[i] = (lambda b: lambda a: b + a)(i)
    ...     
    >>> adders[1](3)
    4
    >>> adders[2](3)
    5

    Buradaki kapsam, bağımsız değişkenini bağlayan ve bağımsız değişken olarak bağlamak istediğiniz değeri geçiren yeni bir işlev (kısalık için bir lambda) kullanılarak oluşturulur. Gerçek kodda, yeni kapsamı oluşturmak için muhtemelen lambda yerine sıradan bir işleve sahip olacaksınız:

    def createAdder(x):
        return lambda y: y + x
    adders = [createAdder(i) for i in range(4)]

1
Max, diğer (daha basit sorum) için bir cevap eklerseniz, bunu kabul edilmiş bir cevap olarak işaretleyebilirim. Teşekkürler!
Boaz

3
Python'un statik kapsamı vardır, dinamik kapsamı yoktur .. sadece tüm değişkenler referanstır, bu nedenle yeni bir nesneye bir değişken ayarladığınızda, değişkenin kendisi (referans) aynı konuma sahiptir, ancak başka bir şeye işaret eder. aynı şey Şema'da da gerçekleşir set!. dinamik kapsamın gerçekte ne olduğuna bakın: voidspace.org.uk/python/articles/code_blocks.shtml .
Claudiu

6
Seçenek 2, hangi işlevsel dillerin "Curried işlevi" olarak adlandırılacağını gösterir.
Crashworks

205

varsayılan değere sahip bir argüman kullanarak bir değişkenin yakalanmasını zorlayabilirsiniz:

>>> for i in [0,1,2,3]:
...    adders[i]=lambda a,i=i: i+a  # note the dummy parameter with a default value
...
>>> print( adders[1](3) )
4

Fikir bir parametre bildirmek (akıllıca adlandırılmış olan i) ve bir daha yakalama istediğiniz değişkenin varsayılan değeri (değeri vermek i)


7
Varsayılan değerleri kullanmak için +1. Lambda tanımlandığında değerlendirilmek onları bu kullanım için mükemmel kılar.
quornian

21
+1, çünkü bu resmi SSS tarafından onaylanan çözümdür .
abarnert

23
Bu harika. Bununla birlikte, varsayılan Python davranışı değildir.
Cecil Curry

1
Bu sadece iyi bir çözüm gibi görünmüyor ... aslında değişkenin bir kopyasını yakalamak için işlev imzasını değiştiriyorsunuz. Ve ayrıca işlevi çağıranlar i değişkeni ile karışabilir, değil mi?
David Callanan

@DavidCallanan bir lambda'dan bahsediyoruz: bir sdk'nin tamamında paylaştığınız bir şey değil, genellikle kendi kodunuzda bir delik takmak için tanımladığınız bir tür geçici işlev. daha güçlü bir imzaya ihtiyacınız varsa, gerçek bir işlev kullanmalısınız.
Adrien Plisson

33

Bütünlüğü için ikinci soruya başka cevap: Sen kullanabilirsiniz kısmi de functools modülü.

Chris Lutz'ın önerdiği gibi operatörden ekleme ekleyerek örnek şöyle olur:

from functools import partial
from operator import add   # add(a, b) -- Same as a + b.

adders = [0,1,2,3]
for i in [0,1,2,3]:
   # store callable object with first argument given as (current) i
   adders[i] = partial(add, i) 

print adders[1](3)

24

Aşağıdaki kodu göz önünde bulundurun:

x = "foo"

def print_x():
    print x

x = "bar"

print_x() # Outputs "bar"

Çoğu insanın bu kafa karıştırıcıyı hiç bulamayacağını düşünüyorum. Beklenen davranıştır.

Peki, insanlar neden bir döngü içinde yapıldığında bunun farklı olacağını düşünüyor? Bu hatayı kendim yaptığımı biliyorum, ama nedenini bilmiyorum. Döngü mü? Ya da belki lambda?

Sonuçta, döngü sadece daha kısa bir versiyonudur:

adders= [0,1,2,3]
i = 0
adders[i] = lambda a: i+a
i = 1
adders[i] = lambda a: i+a
i = 2
adders[i] = lambda a: i+a
i = 3
adders[i] = lambda a: i+a

11
Bu döngüdür, çünkü diğer birçok dilde bir döngü yeni bir kapsam oluşturabilir.
Haziran'da

1
Bu cevap iyidir, çünkü iher lambda işlevi için aynı değişkene neden erişildiğini açıklar .
David Callanan

3

İkinci sorunuza yanıt olarak, bunu yapmanın en zarif yolu, bir dizi yerine iki parametre alan bir işlev kullanmak olacaktır:

add = lambda a, b: a + b
add(1, 3)

Ancak, burada lambda kullanmak biraz saçma. Python bize operatortemel operatörlere fonksiyonel bir arayüz sağlayan modülü veriyor . Yukarıdaki lambda'nın sadece ekleme operatörünü çağırmak için gereksiz yükü vardır:

from operator import add
add(1, 3)

Etrafta oynadığınızı, dili keşfetmeye çalıştığınızı anlıyorum, ancak Python'un kapsam tuhaflığının önüne geçeceği bir dizi işlevi kullanacağımı hayal edemiyorum.

İsterseniz dizi dizine ekleme sözdiziminizi kullanan küçük bir sınıf yazabilirsiniz:

class Adders(object):
    def __getitem__(self, item):
        return lambda a: a + item

adders = Adders()
adders[1](3)

2
Chris, elbette yukarıdaki kodun orijinal sorunumla ilgisi yok. Demek istediğimi basit bir şekilde göstermek için oluşturuldu. Tabii ki anlamsız ve saçma.
Boaz

3

Burada, kapalı bağlamın "kaydedildiğini" açıklığa kavuşturmaya yardımcı olması için bir kapağın veri yapısını ve içeriğini vurgulayan yeni bir örnek verilmiştir.

def make_funcs():
    i = 42
    my_str = "hi"

    f_one = lambda: i

    i += 1
    f_two = lambda: i+1

    f_three = lambda: my_str
    return f_one, f_two, f_three

f_1, f_2, f_3 = make_funcs()

Kapanışta ne var?

>>> print f_1.func_closure, f_1.func_closure[0].cell_contents
(<cell at 0x106a99a28: int object at 0x7fbb20c11170>,) 43 

Özellikle, my_str f1'in kapanışında değil.

F2'nin kapanmasında neler var?

>>> print f_2.func_closure, f_2.func_closure[0].cell_contents
(<cell at 0x106a99a28: int object at 0x7fbb20c11170>,) 43

Her iki kapağın da aynı nesneleri içerdiğine (bellek adreslerinden) dikkat edin. Böylece başlayabilirsiniz lambda fonksiyonunu kapsama referans olarak düşünmeye . Ancak, my_str, f_1 veya f_2 için kapanışta değil ve i, kapatma nesnelerinin kendilerinin ayrı nesneler olduğunu gösteren f_3 (gösterilmiyor) için kapanışta değil.

Kapatma nesneleri kendileri aynı nesne midir?

>>> print f_1.func_closure is f_2.func_closure
False

NB Çıktı int object at [address X]>bana kapanışın [adres X] AKA'yı bir referans olarak sakladığını düşündürdü. Ancak, değişken, lambda deyiminden sonra yeniden atanırsa [adres X] değişecektir.
Jeff
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.