Cakupan fungsi lambda dan parameternya?

92

Saya membutuhkan fungsi panggilan balik yang hampir persis sama untuk serangkaian acara gui. Fungsi ini akan berperilaku sedikit berbeda bergantung pada peristiwa mana yang memanggilnya. Sepertinya kasus yang sederhana bagi saya, tetapi saya tidak dapat memahami perilaku aneh fungsi lambda ini.

Jadi saya memiliki kode yang disederhanakan berikut ini:

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()

Output dari kode ini adalah:

mi
mi
mi
do
re
mi

Saya mengharapkan:

do
re
mi
do
re
mi

Mengapa menggunakan iterator mengacaukan segalanya?

Saya sudah mencoba menggunakan deepcopy:

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

Tetapi ini memiliki masalah yang sama.

agartland
sumber
3
Judul pertanyaan Anda agak menyesatkan.
lispmachine
1
Mengapa menggunakan lambda jika Anda merasa bingung? Mengapa tidak menggunakan def untuk mendefinisikan fungsi? Ada apa dengan masalah Anda yang membuat lambda begitu penting?
S. Lott
@ S.Lott Fungsi bersarang akan menghasilkan masalah yang sama (mungkin lebih terlihat jelas)
lispmachine
1
@agartland: Apakah Anda saya? Saya juga sedang mengerjakan acara GUI, dan saya menulis tes yang hampir identik berikut sebelum menemukan halaman ini selama penelitian latar belakang: pastebin.com/M5jjHjFT
imallett
5
Lihat Mengapa lambda yang didefinisikan dalam sebuah loop dengan nilai yang berbeda semuanya mengembalikan hasil yang sama? di FAQ Pemrograman resmi untuk Python. Ini menjelaskan masalah dengan cukup baik, dan menawarkan solusi.
abarnert

Jawaban:

80

Masalahnya di sini adalah m variabel (referensi) yang diambil dari lingkup sekitarnya. Hanya parameter yang disimpan dalam lingkup lambda.

Untuk mengatasi ini, Anda harus membuat ruang lingkup lain untuk lambda:

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()

Dalam contoh di atas, lambda juga menggunakan cakupan sekitarnya untuk menemukan m, tetapi kali ini callback_factorycakupannya yang dibuat sekali untuk setiap callback_factory panggilan.

Atau dengan functools.pihak :

from functools import partial

def callback(msg):
    print msg

funcList=[partial(callback, m) for m in ('do', 're', 'mi')]
for f in funcList:
    f()
lispmachine
sumber
2
Penjelasan ini agak menyesatkan. Masalahnya adalah perubahan nilai m dalam iterasi, bukan cakupannya.
Ixx
Komentar di atas memang benar seperti yang dicatat oleh @abarnert pada komentar pertanyaan di mana juga diberikan link yang menjelaskan fenonimon dan solusinya. Metode pabrik memberikan efek yang sama seperti argumen ke metode pabrik memiliki efek membuat variabel baru dengan lingkup lokal ke lambda. Namun solisi yang diberikan tidak berfungsi secara sintaksis karena tidak ada argumen ke lambda - dan lambda dalam solusi lamda di bawah ini juga memberikan efek yang sama tanpa membuat metode persisten baru untuk membuat lambda
Mark Parris
134

Ketika lambda dibuat, itu tidak membuat salinan dari variabel dalam lingkup pelingkupan yang digunakannya. Ini memelihara referensi ke lingkungan sehingga dapat mencari nilai variabel nanti. Hanya ada satu m. Itu akan ditugaskan setiap kali melalui loop. Setelah pengulangan, variabel mmemiliki nilai 'mi'. Jadi ketika Anda benar-benar menjalankan fungsi yang Anda buat nanti, itu akan mencari nilai mdi lingkungan yang membuatnya, yang kemudian akan memiliki nilai.'mi' .

Satu solusi umum dan idiomatik untuk masalah ini adalah dengan menangkap nilai mpada saat lambda dibuat dengan menggunakannya sebagai argumen default dari parameter opsional. Anda biasanya menggunakan parameter dengan nama yang sama sehingga Anda tidak perlu mengubah badan kode:

for m in ('do', 're', 'mi'):
    funcList.append(lambda m=m: callback(m))
newacct
sumber
6
Solusi bagus! Meski rumit, saya merasa arti aslinya lebih jelas dibandingkan dengan sintaks lainnya.
Quantum7
3
Tidak ada sama sekali hackish atau tricky tentang ini; itu persis solusi yang disarankan FAQ Python resmi. Lihat disini .
abarnert
3
@abernert, "hackish and tricky" tidak selalu tidak cocok dengan "menjadi solusi yang disarankan FAQ Python resmi". Terima kasih atas referensinya.
Don Hatch
1
menggunakan kembali nama variabel yang sama tidak jelas bagi seseorang yang tidak terbiasa dengan konsep ini. Ilustrasi akan lebih baik jika lambda n = m. Ya, Anda harus mengubah parameter panggilan balik Anda, tetapi badan loop for bisa tetap sama menurut saya.
Nick
1
+1 Sepanjang jalan! Ini harus menjadi solusi ... Solusi yang diterima tidak sebagus yang ini. Bagaimanapun, kami mencoba menggunakan fungsi lambda di sini ... Tidak hanya memindahkan semuanya ke defdeklarasi.
255.tar.xz
6

Python tentu saja menggunakan referensi, tetapi tidak masalah dalam konteks ini.

Saat Anda mendefinisikan lambda (atau fungsi, karena ini adalah perilaku yang sama persis), ini tidak mengevaluasi ekspresi lambda sebelum runtime:

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

broken() # but calling it will raise a NameError

Bahkan lebih mengejutkan dari contoh lambda Anda:

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

foo() # bar

i = 'banana'

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

Singkatnya, berpikirlah dinamis: tidak ada yang dievaluasi sebelum interpretasi, itulah mengapa kode Anda menggunakan nilai terbaru m.

Ketika mencari m dalam eksekusi lambda, m diambil dari ruang lingkup paling atas, yang berarti, seperti yang ditunjukkan orang lain; Anda dapat menghindari masalah itu dengan menambahkan cakupan lain:

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

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

Di sini, ketika lambda dipanggil, itu terlihat dalam cakupan definisi lambda untuk x. X ini adalah variabel lokal yang ditentukan dalam tubuh pabrik. Karenanya, nilai yang digunakan pada eksekusi lambda akan menjadi nilai yang diteruskan sebagai parameter selama panggilan ke pabrik. Dan doremi!

Sebagai catatan, saya bisa saja mendefinisikan pabrik sebagai pabrik (m) [ganti x dengan m], perilakunya sama. Saya menggunakan nama yang berbeda untuk kejelasan :)

Anda mungkin menemukan bahwa Andrej Bauer memiliki masalah lambda yang serupa. Yang menarik di blog itu adalah komentarnya, di mana Anda akan belajar lebih banyak tentang penutupan python :)

Nicolas Dumazet
sumber
1

Tidak secara langsung terkait dengan masalah yang sedang dihadapi, tetapi sebuah kebijaksanaan yang sangat berharga: Objek Python oleh Fredrik Lundh.

tzot.dll
sumber
1
Tidak terkait langsung dengan jawaban Anda, tetapi penelusuran untuk anak kucing: google.com/search?q=kitten
Singletoned
@Singletoned: jika OP membaca artikel yang saya berikan tautannya, mereka tidak akan mengajukan pertanyaan sejak awal; itulah mengapa itu terkait secara tidak langsung. Saya yakin Anda akan dengan senang hati menjelaskan kepada saya bagaimana anak kucing secara tidak langsung terkait dengan jawaban saya (saya kira melalui pendekatan holistik;)
tzot
1

Ya, itu masalah ruang lingkup, itu mengikat m luar, apakah Anda menggunakan lambda atau fungsi lokal. Sebagai gantinya, gunakan functor:

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))
Benoît
sumber
1

soluiton untuk lambda lebih lambda

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

bagian luar lambdadigunakan untuk mengikat nilai saat ini ike j di

setiap kali luar yang lambdadisebut itu membuat sebuah instance dari inner lambdadengan jterikat pada nilai saat ini dari isebagai inilai 's

Aaron Goldman
sumber
0

Pertama, apa yang Anda lihat bukanlah masalah, dan tidak terkait dengan panggilan-dengan-referensi atau dengan-nilai.

Sintaks lambda yang Anda tentukan tidak memiliki parameter, dan karena itu, cakupan yang Anda lihat dengan parameter mberada di luar fungsi lambda. Inilah mengapa Anda melihat hasil ini.

Sintaks lambda, dalam contoh Anda tidak diperlukan, dan Anda lebih suka menggunakan panggilan fungsi sederhana:

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

Sekali lagi, Anda harus sangat tepat tentang parameter lambda apa yang Anda gunakan dan di mana tepatnya ruang lingkupnya dimulai dan diakhiri.

Sebagai catatan tambahan, tentang penyaluran parameter. Parameter dalam python selalu mengacu pada objek. Mengutip Alex Martelli:

Masalah terminologi mungkin karena fakta bahwa, dalam python, nilai nama adalah referensi ke suatu objek. Jadi, Anda selalu meneruskan nilai (tanpa penyalinan implisit), dan nilai itu selalu menjadi referensi. [...] Sekarang jika Anda ingin membuat nama untuk itu, seperti "berdasarkan referensi objek", "dengan nilai yang tidak disalin", atau apa pun, jadilah tamuku. Mencoba menggunakan kembali terminologi yang lebih umum diterapkan ke bahasa di mana "variabel adalah kotak" ke bahasa di mana "variabel adalah tag post-it", IMHO, lebih cenderung membingungkan daripada membantu.

Yuval Adam
sumber
0

Variabel msedang ditangkap, jadi ekspresi lambda Anda selalu melihat nilai "saat ini".

Jika Anda perlu menangkap nilai secara efektif pada suatu waktu, tulis fungsi yang mengambil nilai yang Anda inginkan sebagai parameter, dan mengembalikan ekspresi lambda. Pada titik itu, lambda akan menangkap nilai parameter , yang tidak akan berubah saat Anda memanggil fungsi beberapa kali:

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()

Keluaran:

do
re
mi
Jon Skeet
sumber
0

sebenarnya tidak ada variabel dalam pengertian klasik di Python, hanya nama yang telah terikat oleh referensi ke objek yang berlaku. Fungsi genap adalah semacam objek dengan Python, dan lambda tidak membuat pengecualian pada aturan :)

Tom
sumber
Saat Anda mengatakan "dalam arti klasik", yang Anda maksud adalah "seperti C punya". Banyak bahasa, termasuk Python, menerapkan variabel secara berbeda dari C.
Ned Batchelder
0

Sebagai catatan tambahan, mapmeskipun dibenci oleh beberapa tokoh Python terkenal, memaksa konstruksi yang mencegah jebakan ini.

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

NB: lambda itindakan pertama seperti pabrik di jawaban lain.

YvesgereY
sumber