ขอบเขตของฟังก์ชันแลมด้าและพารามิเตอร์?


92

ฉันต้องการฟังก์ชั่นการโทรกลับที่เกือบจะเหมือนกันทุกประการสำหรับชุดกิจกรรม gui ฟังก์ชันจะทำงานแตกต่างกันเล็กน้อยขึ้นอยู่กับเหตุการณ์ที่เรียกมัน ดูเหมือนจะเป็นกรณีธรรมดาสำหรับฉัน แต่ฉันไม่สามารถเข้าใจพฤติกรรมแปลก ๆ ของฟังก์ชันแลมด้านี้ได้

ดังนั้นฉันจึงมีโค้ดแบบง่ายต่อไปนี้ด้านล่าง:

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

ผลลัพธ์ของรหัสนี้คือ:

mi
mi
mi
do
re
mi

ฉันคาดหวัง:

do
re
mi
do
re
mi

เหตุใดการใช้ iterator จึงทำให้สิ่งต่างๆยุ่งเหยิง

ฉันได้ลองใช้ deepcopy:

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

แต่นี่ก็มีปัญหาเหมือนกัน


3
ชื่อคำถามของคุณค่อนข้างทำให้เข้าใจผิด
lispmachine

1
ทำไมต้องใช้ lambdas ถ้าคุณพบว่ามันสับสน? ทำไมไม่ใช้ def เพื่อกำหนดฟังก์ชัน อะไรเกี่ยวกับปัญหาของคุณที่ทำให้ lambdas สำคัญมาก?
S.Lott

ฟังก์ชั่น @ S.Lott Nested จะทำให้เกิดปัญหาเดียวกัน (อาจมองเห็นได้ชัดเจนกว่า)
lispmachine

1
@agartland: คุณเป็นฉัน? ฉันก็ทำงานเกี่ยวกับเหตุการณ์ GUI เช่นกันและฉันได้เขียนการทดสอบที่เหมือนกันเกือบทั้งหมดต่อไปนี้ก่อนที่จะพบหน้านี้ในระหว่างการค้นคว้าข้อมูลเบื้องหลัง: pastebin.com/M5jjHjFT
imallett

5
ดูเหตุใด lambdas ที่กำหนดในลูปที่มีค่าต่างกันทั้งหมดให้ผลลัพธ์เดียวกัน ในคำถามที่พบบ่อยเกี่ยวกับการเขียนโปรแกรมอย่างเป็นทางการสำหรับ Python มันอธิบายปัญหาได้ดีและเสนอวิธีแก้ปัญหา
ยกเลิก

คำตอบ:


80

ปัญหาคือmตัวแปร (ข้อมูลอ้างอิง) ที่ถูกนำมาจากขอบเขตรอบข้าง เฉพาะพารามิเตอร์เท่านั้นที่อยู่ในขอบเขตแลมบ์ดา

ในการแก้ปัญหานี้คุณต้องสร้างขอบเขตอื่นสำหรับแลมด้า:

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

ในตัวอย่างข้างต้นแลมบ์ดายังใช้ขอบเขตที่คาดเดาได้ในการค้นหาmแต่คราวนี้เป็นcallback_factoryขอบเขตที่สร้างขึ้นหนึ่งครั้งต่อการcallback_factory โทรทุกครั้ง

หรือด้วยfunctools.partial :

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
คำอธิบายนี้ทำให้เข้าใจผิดเล็กน้อย ปัญหาคือการเปลี่ยนแปลงค่าของ m ในการวนซ้ำไม่ใช่ขอบเขต
Ixx

ความคิดเห็นข้างต้นเป็นความจริงตามที่ระบุไว้โดย @abarnert ในความคิดเห็นในคำถามที่มีลิงค์ที่อธิบายฟีโนนิมอนและวิธีแก้ปัญหา วิธีการโรงงานให้ผลเช่นเดียวกับการโต้แย้งวิธีการโรงงานมีผลในการสร้างตัวแปรใหม่ที่มีขอบเขตโลคัลเป็นแลมด้า อย่างไรก็ตามการแก้ปัญหาที่ให้มาไม่ได้ผลในเชิงสังเคราะห์เนื่องจากไม่มีข้อโต้แย้งใด ๆ กับแลมด้า - และแลมด้าในโซลูชันแลมด้าด้านล่างก็ให้ผลเช่นเดียวกันโดยไม่ต้องสร้างวิธีการถาวรใหม่เพื่อสร้างแลมด้า
Mark Parris

134

เมื่อสร้างแลมบ์ดาจะไม่สร้างสำเนาของตัวแปรในขอบเขตการปิดล้อมที่ใช้ มันรักษาการอ้างอิงถึงสภาพแวดล้อมเพื่อให้สามารถค้นหาค่าของตัวแปรได้ในภายหลัง mมีเพียงหนึ่ง จะได้รับมอบหมายให้ทุกครั้งผ่านลูป หลังจากที่วงตัวแปรมีค่าm 'mi'ดังนั้นเมื่อคุณเรียกใช้ฟังก์ชันที่คุณสร้างขึ้นในภายหลังมันจะค้นหาค่าmในสภาพแวดล้อมที่สร้างขึ้นซึ่งจะมีค่า'mi'อยู่ในสภาพแวดล้อมที่สร้างมันซึ่งจากนั้นจะมีค่า

วิธีแก้ปัญหาทั่วไปและสำนวนหนึ่งสำหรับปัญหานี้คือการจับค่าของmเวลาที่สร้างแลมบ์ดาโดยใช้เป็นอาร์กิวเมนต์เริ่มต้นของพารามิเตอร์ที่เป็นทางเลือก โดยปกติคุณจะใช้พารามิเตอร์ที่มีชื่อเดียวกันดังนั้นคุณจึงไม่จำเป็นต้องเปลี่ยนเนื้อหาของโค้ด:

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

6
ทางออกที่ดี! แม้ว่าจะยุ่งยาก แต่ฉันรู้สึกว่าความหมายดั้งเดิมนั้นชัดเจนกว่าไวยากรณ์อื่น ๆ
Quantum7

3
ไม่มีอะไรที่แฮ็คหรือยุ่งยากเกี่ยวกับเรื่องนี้ เป็นวิธีแก้ปัญหาเดียวกันกับที่คำถามที่พบบ่อยอย่างเป็นทางการของ Python แนะนำ ดูที่นี่ .
ยกเลิก

3
@abernert "แฮ็กและมีเล่ห์เหลี่ยม" ไม่จำเป็นต้องเข้ากันไม่ได้กับ "การเป็นวิธีแก้ปัญหาตามที่ Python FAQ แนะนำ" ขอบคุณสำหรับข้อมูลอ้างอิง
Don Hatch

1
การนำชื่อตัวแปรเดิมกลับมาใช้ซ้ำยังไม่ชัดเจนสำหรับคนที่ไม่คุ้นเคยกับแนวคิดนี้ ภาพประกอบจะดีกว่าถ้าเป็น lambda n = m ใช่คุณต้องเปลี่ยนพารามิเตอร์การเรียกกลับของคุณ แต่สำหรับร่างกายสำหรับลูปอาจยังคงเหมือนเดิมฉันคิดว่า
นิค

1
+1 ตลอดทาง! นี่ควรเป็นทางออก ... วิธีแก้ปัญหาที่ได้รับการยอมรับนั้นไม่ดีเท่าวิธีนี้ defท้ายที่สุดเรากำลังพยายามใช้ฟังก์ชันแลมด้าที่นี่ ... ไม่ใช่แค่ย้ายทุกอย่างไปสู่การประกาศเท่านั้น
255.tar.xz

6

Python ใช้การอ้างอิงแน่นอน แต่ไม่สำคัญในบริบทนี้

เมื่อคุณกำหนดแลมบ์ดา (หรือฟังก์ชันเนื่องจากนี่เป็นลักษณะการทำงานเดียวกันทุกประการ) จะไม่ประเมินนิพจน์แลมด้าก่อนรันไทม์:

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

broken() # but calling it will raise a NameError

น่าแปลกใจยิ่งกว่าตัวอย่างแลมด้าของคุณ:

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

foo() # bar

i = 'banana'

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

ในระยะสั้นคิดว่าไดนามิก: ไม่มีการประเมินก่อนการตีความนั่นคือสาเหตุที่โค้ดของคุณใช้ค่าล่าสุดของ m

เมื่อมองหา m ในการดำเนินการแลมบ์ดา m จะถูกนำมาจากขอบเขตบนสุดซึ่งหมายความว่าตามที่คนอื่น ๆ ชี้ให้เห็น คุณสามารถหลีกเลี่ยงปัญหานั้นได้โดยเพิ่มขอบเขตอื่น:

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

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

ที่นี่เมื่อมีการเรียกแลมบ์ดามันจะดูในขอบเขตนิยามแลมบ์ดาสำหรับ x x นี้เป็นตัวแปรท้องถิ่นที่กำหนดไว้ในตัวของโรงงาน ด้วยเหตุนี้ค่าที่ใช้ในการดำเนินการแลมบ์ดาจะเป็นค่าที่ส่งผ่านเป็นพารามิเตอร์ระหว่างการเรียกไปยังโรงงาน และโดเรมี!

ตามหมายเหตุฉันสามารถกำหนดโรงงานเป็นโรงงาน (m) [แทนที่ x ด้วย m] พฤติกรรมก็เหมือนกัน ฉันใช้ชื่ออื่นเพื่อความชัดเจน :)

คุณอาจพบว่าAndrej Bauerมีปัญหาแลมด้าที่คล้ายกัน สิ่งที่น่าสนใจในบล็อกนั้นคือความคิดเห็นซึ่งคุณจะได้เรียนรู้เพิ่มเติมเกี่ยวกับการปิด python :)


1

ไม่เกี่ยวข้องโดยตรงกับปัญหาที่อยู่ในมือ แต่เป็นภูมิปัญญาล้ำค่าอย่างไรก็ตามPython Objectsโดย Fredrik Lundh


1
ไม่เกี่ยวข้องโดยตรงกับคำตอบของคุณ แต่ค้นหาลูกแมว: google.com/search?q=kitten
Singletoned

@Singletoned: ถ้า OP บ่นบทความที่ฉันให้ลิงค์ไว้พวกเขาจะไม่ถามคำถามตั้งแต่แรก นั่นคือเหตุผลว่าทำไมจึงเกี่ยวข้องโดยอ้อม ฉันแน่ใจว่าคุณจะยินดีที่จะอธิบายให้ฉันฟังว่าลูกแมวมีความสัมพันธ์ทางอ้อมกับคำตอบของฉันอย่างไร (ด้วยวิธีการแบบองค์รวมฉันคิดว่า;)
tzot

1

ใช่นั่นเป็นปัญหาของขอบเขตมันผูกกับ m ภายนอกไม่ว่าคุณจะใช้แลมด้าหรือฟังก์ชันโลคัล ใช้ 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))

1

โซลิตันถึงแลมบ์ดาเป็นแลมด้ามากกว่า

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

ด้านนอกlambdaใช้เพื่อผูกค่าปัจจุบันของiถึงj ที่

ในแต่ละครั้งที่ด้านนอกlambdaที่เรียกว่ามันทำให้ตัวอย่างของภายในlambdaกับjผูกไว้กับค่าปัจจุบันของiเป็นiค่า 's


0

อันดับแรกสิ่งที่คุณเห็นไม่ใช่ปัญหาและไม่เกี่ยวข้องกับการเรียกตามการอ้างอิงหรือตามค่า

ไวยากรณ์แลมบ์ดาที่คุณกำหนดไม่มีพารามิเตอร์ดังนั้นขอบเขตที่คุณเห็นด้วยพารามิเตอร์ mจึงอยู่ภายนอกของฟังก์ชันแลมบ์ดา นี่คือเหตุผลที่คุณเห็นผลลัพธ์เหล่านี้

ไวยากรณ์ Lambda ในตัวอย่างของคุณไม่จำเป็นและคุณต้องการใช้การเรียกใช้ฟังก์ชันง่ายๆ:

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

ขอย้ำอีกครั้งว่าคุณใช้พารามิเตอร์แลมบ์ดาอย่างชัดเจนและขอบเขตเริ่มต้นและสิ้นสุดที่ใด

เป็นหมายเหตุด้านข้างเกี่ยวกับการส่งผ่านพารามิเตอร์ พารามิเตอร์ใน python อ้างอิงถึงวัตถุเสมอ เพื่ออ้างถึง Alex Martelli:

ปัญหาเกี่ยวกับศัพท์อาจเกิดจากข้อเท็จจริงที่ว่าใน python ค่าของชื่อเป็นการอ้างอิงถึงวัตถุ ดังนั้นคุณมักจะส่งผ่านค่า (ไม่มีการคัดลอกโดยนัย) และค่านั้นจะเป็นข้อมูลอ้างอิงเสมอ [... ] ตอนนี้ถ้าคุณต้องการเหรียญชื่อสำหรับสิ่งนั้นเช่น "โดยการอ้างอิงวัตถุ" "ตามค่าที่ไม่ได้คัดลอก" หรืออะไรก็ตามเป็นแขกของฉัน การพยายามใช้คำศัพท์ซ้ำที่มักใช้กับภาษาที่ "ตัวแปรเป็นกล่อง" กับภาษาที่ "ตัวแปรเป็นแท็กโพสต์อิท" คือ IMHO มีแนวโน้มที่จะสับสนมากกว่าที่จะช่วยได้


0

ตัวแปร mกำลังจับดังนั้นนิพจน์แลมบ์ดาของคุณจะเห็นค่า "ปัจจุบัน" เสมอ

หากคุณต้องการจับค่าอย่างมีประสิทธิภาพในช่วงเวลาหนึ่งให้เขียนฟังก์ชันรับค่าที่คุณต้องการเป็นพารามิเตอร์และส่งกลับนิพจน์แลมบ์ดา เมื่อถึงจุดนั้นแลมด้าจะจับค่าของพารามิเตอร์ซึ่งจะไม่เปลี่ยนแปลงเมื่อคุณเรียกใช้ฟังก์ชันหลายครั้ง:

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

เอาท์พุต:

do
re
mi

0

ไม่มีตัวแปรในความหมายคลาสสิกใน Python มีเพียงชื่อที่ถูกผูกไว้โดยการอ้างอิงถึงวัตถุที่เกี่ยวข้อง แม้แต่ฟังก์ชั่นก็เป็นวัตถุบางประเภทใน Python และ lambdas ไม่ได้ยกเว้นกฎ :)


เมื่อคุณพูดว่า "ในความหมายคลาสสิก" คุณหมายถึง "เหมือน C" หลายภาษารวมถึง Python ใช้ตัวแปรต่างจาก C.
Ned Batchelder

0

ในฐานะที่เป็นข้อสังเกตด้านข้างmapแม้ว่าจะดูหมิ่นร่าง Python ที่รู้จักกันดี แต่ก็บังคับให้มีการก่อสร้างที่ป้องกันข้อผิดพลาดนี้

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

หมายเหตุ: ข้อแรกlambda iทำตัวเหมือนโรงงานในคำตอบอื่น ๆ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.