ฟังก์ชั่น (แลมบ์ดา) ปิดการจับภาพอย่างไร


249

เมื่อเร็ว ๆ นี้ฉันเริ่มเล่นกับ Python และฉันพบสิ่งแปลก ๆ ในวิธีการปิดการทำงาน พิจารณารหัสต่อไปนี้:

adders=[0,1,2,3]

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

print adders[1](3)

มันสร้างอาเรย์อย่างง่ายของฟังก์ชั่นที่รับอินพุตเดี่ยวและส่งคืนอินพุตที่เพิ่มโดยตัวเลข ฟังก์ชั่นที่มีการสร้างขึ้นในforวงที่การทำซ้ำโดยiวิ่งจากไป0 3สำหรับตัวเลขเหล่านี้แต่ละlambdaฟังก์ชั่นจะถูกสร้างขึ้นเพื่อรวบรวมiและเพิ่มเข้าไปในอินพุตของฟังก์ชัน บรรทัดสุดท้ายเรียกใช้lambdaฟังก์ชันที่สองโดย3ใช้เป็นพารามิเตอร์ 6ที่แปลกใจของการส่งออกเป็น

4ผมคาดว่า เหตุผลของฉันคือ: ใน Python ทุกอย่างเป็นวัตถุดังนั้นทุกตัวแปรจึงเป็นตัวชี้ที่สำคัญสำหรับมัน เมื่อสร้างlambdaปิดสำหรับผมคาดว่ามันเก็บชี้ไปยังวัตถุจำนวนเต็มชี้ปัจจุบันโดยi iนั่นหมายความว่าเมื่อiกำหนดวัตถุจำนวนเต็มใหม่มันจะไม่ส่งผลต่อการปิดที่สร้างไว้ก่อนหน้านี้ น่าเศร้าที่การตรวจสอบaddersอาร์เรย์ภายในโปรแกรมดีบั๊กเกอร์แสดงว่าทำเช่นนั้น ทุกlambdaฟังก์ชั่นหมายถึงค่าสุดท้ายของi, 3ซึ่งส่งผลให้กลับมาadders[1](3)6

ซึ่งทำให้ฉันสงสัยเกี่ยวกับสิ่งต่อไปนี้:

  • สิ่งที่ปิดการจับภาพที่แน่นอน?
  • เป็นวิธีที่สวยงามที่สุดในการโน้มน้าวให้lambdaฟังก์ชั่นในการจับมูลค่าปัจจุบันiในทางที่จะไม่ได้รับผลกระทบเมื่อiเปลี่ยนค่าของมันคืออะไร?

35
ฉันมีปัญหานี้ในรหัส UI ผลักฉันถั่ว เคล็ดลับคือการจำลูปไม่ได้สร้างขอบเขตใหม่
det

3
@TimMB จะiออกจากเนมสเปซได้อย่างไร
detly

3
@detly ดีฉันจะบอกว่าprint iจะไม่ทำงานหลังจากวง แต่ฉันทดสอบด้วยตัวเองและตอนนี้ฉันเห็นสิ่งที่คุณหมายถึง - มันใช้งานได้ ฉันไม่ทราบว่าตัวแปรวนซ้ำตามร่างกายวนในหลาม
ทิม MB

1
@TimMB - ใช่นั่นคือสิ่งที่ฉันหมายถึง เหมือนกันสำหรับif, with, tryฯลฯ
detly

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

คำตอบ:


161

คำถามที่สองของคุณได้รับคำตอบแล้ว แต่สำหรับคำถามแรกของคุณ:

การปิดนั้นจับอะไรกันแน่?

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

แก้ไข: สำหรับคำถามอื่น ๆ ของคุณเกี่ยวกับวิธีเอาชนะปัญหานี้มีสองวิธีที่คุณนึกถึง:

  1. รัดกุมมากที่สุด แต่วิธีที่ไม่เทียบเท่าอย่างเคร่งครัดเป็นอย่างใดอย่างหนึ่งที่แนะนำโดย Adrien Plisson สร้างแลมบ์ดาด้วยอาร์กิวเมนต์พิเศษและตั้งค่าเริ่มต้นของอาร์กิวเมนต์พิเศษเป็นวัตถุที่คุณต้องการเก็บรักษาไว้

  2. รายละเอียดเพิ่มเติมเล็กน้อย แต่การแฮ็กน้อยกว่านั้นคือการสร้างขอบเขตใหม่ทุกครั้งที่คุณสร้างแลมบ์ดา:

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

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

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

1
สูงสุดถ้าคุณเพิ่มคำตอบสำหรับคำถามอื่น ๆ ของฉัน (คำถามที่ง่ายกว่า) ฉันสามารถทำเครื่องหมายคำตอบนี้เป็นคำตอบที่ยอมรับได้ ขอบคุณ!
โบอาซ

3
Python มีการกำหนดขอบเขตแบบคงที่ไม่ใช่การกำหนดขอบเขตแบบไดนามิก .. มันเป็นเพียงตัวแปรทั้งหมดที่มีการอ้างอิงดังนั้นเมื่อคุณตั้งค่าตัวแปรเป็นวัตถุใหม่ตัวแปรตัวเอง (การอ้างอิง) มีตำแหน่งเดียวกัน แต่ชี้ไปที่สิ่งอื่น set!สิ่งเดียวกันที่เกิดขึ้นในโครงการถ้าคุณ ดูที่นี่สำหรับสิ่งที่ขอบเขตแบบไดนามิกจริงๆคือ: voidspace.org.uk/python/articles/code_blocks.shtml
Claudiu

6
ตัวเลือกที่ 2 คล้ายกับภาษาที่ใช้งานได้จะเรียกว่า "ฟังก์ชั่น Curried"
Crashworks

205

คุณอาจบังคับให้จับตัวแปรโดยใช้อาร์กิวเมนต์ที่มีค่าเริ่มต้น:

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

แนวคิดคือการประกาศพารามิเตอร์ (ตั้งชื่ออย่างชาญฉลาดi) และให้ค่าเริ่มต้นของตัวแปรที่คุณต้องการดักจับ (ค่า i)


7
+1 สำหรับการใช้ค่าเริ่มต้น การประเมินผลเมื่อแลมบ์ดาถูกกำหนดทำให้สมบูรณ์แบบสำหรับการใช้งานนี้
quornian

21
+1 ยังเพราะนี่เป็นวิธีการรับรองโดยคำถามที่พบบ่อยอย่างเป็นทางการ
abarnert

23
สิ่งนี้ช่างมหัศจรรย์. อย่างไรก็ตามพฤติกรรมของ Python ที่เป็นค่าเริ่มต้นนั้นไม่ใช่
เซซิลแกงกะหรี่

1
ดูเหมือนจะไม่เป็นทางออกที่ดี แต่ ... คุณกำลังเปลี่ยนลายเซ็นของฟังก์ชั่นเพื่อจับสำเนาของตัวแปร และผู้ที่เรียกใช้ฟังก์ชั่นสามารถยุ่งกับตัวแปร i ได้ไหม?
David Callanan

@DavidCallanan เรากำลังพูดถึงแลมบ์ดา: ฟังก์ชั่น ad-hoc ประเภทที่คุณมักจะกำหนดในรหัสของคุณเองเพื่อเสียบรูไม่ใช่สิ่งที่คุณแบ่งปันผ่าน sdk ทั้งหมด หากคุณต้องการลายเซ็นที่แข็งแกร่งคุณควรใช้ฟังก์ชั่นจริง
Adrien Plisson

33

เพื่อความสมบูรณ์อีกคำตอบสำหรับคำถามที่สองของคุณ: คุณสามารถใช้บางส่วนในโมดูลfunctools

ด้วยการนำเข้าเพิ่มจากโอเปอเรเตอร์ขณะที่ Chris Lutz เสนอตัวอย่างให้:

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

พิจารณารหัสต่อไปนี้:

x = "foo"

def print_x():
    print x

x = "bar"

print_x() # Outputs "bar"

ฉันคิดว่าคนส่วนใหญ่จะไม่พบความสับสนนี้เลย มันเป็นพฤติกรรมที่คาดหวัง

ดังนั้นทำไมผู้คนจึงคิดว่ามันจะแตกต่างกันเมื่อมีการวนซ้ำกัน? ฉันรู้ว่าฉันทำผิดพลาดไปเอง แต่ฉันไม่รู้ว่าทำไม มันคือวง? หรือบางทีแลมบ์ดา?

ท้ายที่สุดการวนซ้ำเป็นเพียงเวอร์ชั่นย่อของ:

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
มันคือการวนซ้ำเพราะในหลาย ๆ ภาษาวงจะสามารถสร้างขอบเขตใหม่ได้
det

1
คำตอบนี้ดีเพราะอธิบายว่าเหตุใดจึงiมีการเข้าถึงตัวแปรเดียวกันสำหรับแต่ละแลมบ์ดา
David Callanan

3

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

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

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

from operator import add
add(1, 3)

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

หากคุณต้องการคุณสามารถเขียนคลาสขนาดเล็กที่ใช้ไวยากรณ์การจัดทำดัชนีอาร์เรย์ของคุณ:

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

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

2
Chris แน่นอนว่าโค้ดข้างต้นไม่เกี่ยวข้องกับปัญหาเดิมของฉัน มันถูกสร้างขึ้นเพื่อแสดงให้เห็นถึงจุดของฉันในวิธีที่ง่าย แน่นอนว่ามันไร้สาระและไร้สาระ
โบอาซ

3

นี่คือตัวอย่างใหม่ที่เน้นโครงสร้างข้อมูลและเนื้อหาของการปิดเพื่อช่วยให้ชัดเจนเมื่อบริบทที่ล้อมรอบถูก "บันทึก"

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

การปิดคืออะไร

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

ยวด my_str ไม่ได้อยู่ในการปิดของ f1

การปิดของ f2 คืออะไร

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

แจ้งให้ทราบ (จากที่อยู่หน่วยความจำ) ว่าการปิดทั้งสองมีวัตถุเดียวกัน ดังนั้นคุณสามารถเริ่มนึกถึงฟังก์ชั่นแลมบ์ดาว่ามีการอ้างอิงขอบเขต อย่างไรก็ตาม my_str ไม่ได้อยู่ในการปิดสำหรับ f_1 หรือ f_2 และฉันไม่ได้อยู่ในการปิดสำหรับ f_3 (ไม่แสดง) ซึ่งแนะนำว่าวัตถุที่ปิดตัวเองนั้นเป็นวัตถุที่แตกต่างกัน

วัตถุปิดตัวเองเป็นวัตถุเดียวกันหรือไม่?

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

NB ผลลัพธ์int object at [address X]>ทำให้ฉันคิดว่าการปิดกำลังเก็บ [อ้างอิง X] AKA เป็นข้อมูลอ้างอิง อย่างไรก็ตาม [address X] จะเปลี่ยนแปลงหากตัวแปรถูกกำหนดใหม่หลังจากคำสั่ง lambda
Jeff
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.