Lambda fonksiyonlarının kapsamı ve parametreleri?


92

Bir dizi gui olayı için neredeyse tamamen aynı olan bir geri arama işlevine ihtiyacım var. İşlev, onu çağıran olaya bağlı olarak biraz farklı davranacaktır. Bana basit bir durum gibi görünüyor, ancak lambda fonksiyonlarının bu garip davranışını çözemiyorum.

Bu yüzden aşağıdaki basitleştirilmiş koda sahibim:

def callback(msg):
    print msg

#creating a list of function handles with an iterator
funcList=[]
for m in ('do', 're', 'mi'):
    funcList.append(lambda: callback(m))
for f in funcList:
    f()

#create one at a time
funcList=[]
funcList.append(lambda: callback('do'))
funcList.append(lambda: callback('re'))
funcList.append(lambda: callback('mi'))
for f in funcList:
    f()

Bu kodun çıktısı:

mi
mi
mi
do
re
mi

Tahmin etmiştim:

do
re
mi
do
re
mi

Yineleyici kullanmak işleri neden alt üst etti?

Deepcopy kullanmayı denedim:

import copy
funcList=[]
for m in ('do', 're', 'mi'):
    funcList.append(lambda: callback(copy.deepcopy(m)))
for f in funcList:
    f()

Ama bunda da aynı sorun var.


3
Soru başlığınız biraz yanıltıcı.
lispmachine

1
Kafa karıştırıcı buluyorsanız neden lambdaları kullanıyorsunuz? Fonksiyonları tanımlamak için neden def kullanılmıyor? Lambdaları bu kadar önemli kılan sorunun nedir?
S.Lott

@ S.Lott İç içe geçmiş işlevi aynı soruna neden olur (belki daha net görülebilir)
lispmachine

1
@agartland: Ben misin? Ben de GUI olayları üzerinde çalışıyordum ve arka plan araştırması sırasında bu sayfayı bulmadan önce aşağıdaki neredeyse aynı testi yazdım: pastebin.com/M5jjHjFT
imallett

5
Bkz. Neden farklı değerlere sahip bir döngüde tanımlanan lambdaların tümü aynı sonucu döndürür? Python için resmi Programlama SSS bölümünde. Sorunu oldukça güzel açıklıyor ve bir çözüm sunuyor.
abarnert

Yanıtlar:


80

Buradaki sorun m, çevreleyen kapsamdan alınan değişkendir (referans). Lambda kapsamında yalnızca parametreler tutulur.

Bunu çözmek için lambda için başka bir kapsam yaratmalısınız:

def callback(msg):
    print msg

def callback_factory(m):
    return lambda: callback(m)

funcList=[]
for m in ('do', 're', 'mi'):
    funcList.append(callback_factory(m))
for f in funcList:
    f()

Yukarıdaki örnekte lambda, bulmak için çevreleyen kapsamı da kullanır m, ancak bu sefer callback_factoryher callback_factory aramada bir kez oluşturulan kapsamdır .

Veya functools.partial ile :

from functools import partial

def callback(msg):
    print msg

funcList=[partial(callback, m) for m in ('do', 're', 'mi')]
for f in funcList:
    f()

2
Bu açıklama biraz yanıltıcıdır. Sorun, kapsam değil, yinelemedeki m değerinin değişmesidir.
Ixx

Yukarıdaki yorum fenonimon ve çözümü açıklayan bir bağlantının verildiği soruya ilişkin yorumda @abarnert tarafından belirtildiği gibi doğrudur. Fabrika yöntemi, fabrika yönteminin bağımsız değişkeninin lambda'ya yerel kapsamda yeni bir değişken oluşturma etkisine sahip olmasıyla aynı etkiyi sağlar. Bununla birlikte, verilen çözüm, lambda için hiçbir argüman olmadığından sözdizimsel olarak çalışmaz - ve aşağıdaki lambda in lamda çözümü, bir lambda oluşturmak için yeni bir kalıcı yöntem oluşturmadan aynı etkiyi sağlar
Mark Parris

134

Bir lambda oluşturulduğunda, kullandığı kapsama alanındaki değişkenlerin bir kopyasını oluşturmaz. Daha sonra değişkenin değerini arayabilmesi için ortama bir referans sağlar. Sadece bir tane var m. Döngü boyunca her seferinde atanır. Döngüden sonra değişkenin mdeğeri vardır 'mi'. Bu nedenle, daha sonra yarattığınız işlevi gerçekten çalıştırdığınızda, onu myaratan ortamdaki değerini arayacak ve o zamana kadar değeri olacaktır 'mi'.

Bu sorunun yaygın ve deyimsel bir çözümü m, lambda'nın oluşturulduğu andaki değerini, isteğe bağlı bir parametrenin varsayılan bağımsız değişkeni olarak kullanarak yakalamaktır . Genellikle aynı isimde bir parametre kullanırsınız, böylece kodun gövdesini değiştirmenize gerek kalmaz:

for m in ('do', 're', 'mi'):
    funcList.append(lambda m=m: callback(m))

6
Güzel çözüm! Zor olmasına rağmen, orijinal anlamın diğer sözdizimlerinden daha net olduğunu hissediyorum.
Quantum7

3
Bunda hiçbir şekilde hileli veya aldatıcı hiçbir şey yok; resmi Python SSS'nin önerdiği çözümün aynısı. Buraya bakın .
abarnert

3
@abernert, "hackish and tricky", "resmi Python SSS'nin önerdiği çözüm olma" ile her zaman uyumsuz değildir. Referans için teşekkürler.
Don Hatch

1
aynı değişken adını tekrar kullanmak, bu kavrama aşina olmayan biri için net değildir. Lambda n = m olsaydı illüstrasyon daha iyi olurdu. Evet, geri arama parametrenizi değiştirmeniz gerekir, ancak for döngüsü gövdesi aynı kalabilir sanırım.
Nick

1
+1 Her şekilde! Çözüm bu olmalı ... Kabul edilen çözüm, bu kadar iyi değil. Sonuçta, burada lambda işlevlerini kullanmaya çalışıyoruz ... Her şeyi bir defbildirime taşımak değil .
255.tar.xz

6

Python elbette referanslar kullanır, ancak bu bağlamda önemi yoktur.

Bir lambda (veya bir işlev, bu tam olarak aynı davranış olduğu için) tanımladığınızda, çalışma zamanından önce lambda ifadesini değerlendirmez:

# defining that function is perfectly fine
def broken():
    print undefined_var

broken() # but calling it will raise a NameError

Lambda örneğinizden daha da şaşırtıcı:

i = 'bar'
def foo():
    print i

foo() # bar

i = 'banana'

foo() # you would expect 'bar' here? well it prints 'banana'

Kısacası, dinamik düşünün: yorumlamadan önce hiçbir şey değerlendirilmez, bu nedenle kodunuz en son m değerini kullanır.

Lambda uygulamasında m aradığında, m en üst kapsamdan alınır, yani diğerlerinin de belirttiği gibi; başka bir kapsam ekleyerek bu sorunu aşabilirsiniz:

def factory(x):
    return lambda: callback(x)

for m in ('do', 're', 'mi'):
    funcList.append(factory(m))

Burada, lambda çağrıldığında, bir x için lambda 'tanım kapsamına bakar. Bu x, fabrikanın gövdesinde tanımlanan yerel bir değişkendir. Bu nedenle lambda çalıştırmada kullanılan değer, fabrikaya çağrı sırasında parametre olarak aktarılan değer olacaktır. Ve doremi!

Bir not olarak, fabrikayı fabrika (m) [x'i m ile değiştir] olarak tanımlayabilirdim, davranış aynıdır. Netlik için farklı bir isim kullandım :)

Andrej Bauer'in de benzer lambda problemleri yaşadığını görebilirsiniz . Bu blogda ilginç olan, python kapatma hakkında daha fazla şey öğreneceğiniz yorumlardır :)


1

Doğrudan eldeki konuyla ilgili değil, ancak yine de paha biçilmez bir bilgelik parçası: Fredrik Lundh'un Python Nesneleri .


1
Doğrudan cevabınızla ilgili değil, ancak kedi yavruları için bir arama: google.com/search?q=kitten
tonlu

@Singletoned: OP, bağlantı verdiğim makaleyi okursa, soruyu ilk etapta sormazlardı; bu nedenle dolaylı olarak ilişkilidir. Eminim yavru kedilerin
cevabımla

1

Evet, bu bir kapsam sorunu, lambda ya da yerel bir işlev kullanıyor olsanız da, dış m'ye bağlanır. Bunun yerine bir functor kullanın:

class Func1(object):
    def __init__(self, callback, message):
        self.callback = callback
        self.message = message
    def __call__(self):
        return self.callback(self.message)
funcList.append(Func1(callback, m))

1

Lambda'ya olan çözüm daha fazla lambda'dır

In [0]: funcs = [(lambda j: (lambda: j))(i) for i in ('do', 're', 'mi')]

In [1]: funcs
Out[1]: 
[<function __main__.<lambda>>,
 <function __main__.<lambda>>,
 <function __main__.<lambda>>]

In [2]: [f() for f in funcs]
Out[2]: ['do', 're', 'mi']

Dış lambdamevcut değerini bağlamak için kullanılır iiçin j de

her seferinde dış lambda denilen iç bir örneğini sağlar lambdaile jmevcut değerine bağlı iolarak ibireyin değeri


0

İlk olarak, gördüğünüz şey bir sorun değildir ve çağrıya göre veya değere göre ilgili değildir.

Tanımladığınız lambda sözdiziminin hiçbir parametresi yoktur ve bu nedenle, parametreyle gördüğünüz kapsam m , lambda işlevinin . Bu yüzden bu sonuçları görüyorsunuz.

Örneğinizde Lambda sözdizimi gerekli değildir ve basit bir işlev çağrısı kullanmayı tercih edersiniz:

for m in ('do', 're', 'mi'):
    callback(m)

Yine, hangi lambda parametrelerini kullandığınız ve kapsamlarının tam olarak nerede başladığı ve bittiği konusunda çok kesin olmalısınız.

Parametre geçişiyle ilgili bir yan not olarak. Python'daki parametreler her zaman nesnelere referanslardır. Alex Martelli'den alıntı yapacak olursak:

Terminoloji sorunu, python'da bir adın değerinin bir nesneye referans olması gerçeğinden kaynaklanıyor olabilir. Dolayısıyla, değeri her zaman iletirsiniz (örtük kopyalama yok) ve bu değer her zaman bir referanstır. [...] Şimdi bunun için "nesne referansıyla", "kopyalanmamış değere göre" veya her neyse, gibi bir isim yazmak istiyorsanız, konuğum olun. Daha genel olarak "değişkenlerin kutulardır" olduğu ve "değişkenlerin post-it etiketleri olduğu" dillere uygulanan terminolojiyi yeniden kullanmaya çalışmak, IMHO, yardımcı olmaktan çok kafa karıştırıcı olabilir.


0

Değişken m yakalanır, bu nedenle lambda ifadeniz her zaman "mevcut" değerini görür.

Bir anda değeri etkili bir şekilde yakalamanız gerekiyorsa, bir işlev yazın, istediğiniz değeri parametre olarak alır ve bir lambda ifadesi döndürür. Bu noktada, lambda parametrenin değerini yakalayacak ve işlevi birden çok kez çağırdığınızda değişmeyecek:

def callback(msg):
    print msg

def createCallback(msg):
    return lambda: callback(msg)

#creating a list of function handles with an iterator
funcList=[]
for m in ('do', 're', 'mi'):
    funcList.append(createCallback(m))
for f in funcList:
    f()

Çıktı:

do
re
mi

0

Python'da klasik anlamda hiçbir değişken yoktur, sadece uygulanabilir nesneye referanslarla bağlanmış adlar vardır. İşlevler bile Python'da bir tür nesnedir ve lambdalar kurala bir istisna yapmaz :)


"Klasik anlamda" derken, "C'nin sahip olduğu gibi" demek istiyorsun. Python da dahil olmak üzere birçok dil değişkenleri C'den farklı şekilde uygular.
Ned Batchelder

0

Bir yan not olarak, mapbazı tanınmış Python figürleri tarafından hor görülse de, bu tuzağı önleyen bir yapıyı zorlar.

fs = map (lambda i: lambda: callback (i), ['do', 're', 'mi'])

Not: lambda idiğer cevaplarda ilk fabrika gibi davranır.

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.