ตัวแปรภายในในฟังก์ชันที่ซ้อนกัน


105

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

from functools import partial

class Cage(object):
    def __init__(self, animal):
        self.animal = animal

def gotimes(do_the_petting):
    do_the_petting()

def get_petters():
    for animal in ['cow', 'dog', 'cat']:
        cage = Cage(animal)

        def pet_function():
            print "Mary pets the " + cage.animal + "."

        yield (animal, partial(gotimes, pet_function))

funs = list(get_petters())

for name, f in funs:
    print name + ":", 
    f()

ให้:

cow: Mary pets the cat.
dog: Mary pets the cat.
cat: Mary pets the cat.

โดยพื้นฐานแล้วทำไมฉันไม่ได้รับสัตว์สามชนิดที่แตกต่างกัน? ไม่ได้เป็นcage'บรรจุเข้าไปในขอบเขตท้องถิ่นของฟังก์ชันซ้อนกันหรือไม่ ถ้าไม่การเรียกใช้ฟังก์ชันซ้อนจะค้นหาตัวแปรภายในได้อย่างไร

ฉันรู้ว่าการประสบปัญหาประเภทนี้มักจะหมายความว่ามีคนหนึ่ง "ทำผิด" แต่ฉันอยากเข้าใจว่าเกิดอะไรขึ้น


1
ลองfor animal in ['cat', 'dog', 'cow']... ฉันแน่ใจว่าจะมีคนมาอธิบายเรื่องนี้ - มันเป็นหนึ่งใน Python gotcha ของ :)
Jon Clements

คำตอบ:


114

ฟังก์ชันที่ซ้อนกันจะค้นหาตัวแปรจากขอบเขตหลักเมื่อดำเนินการไม่ใช่เมื่อกำหนด

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

เมื่อคุณจริงเรียกใช้ฟังก์ชันปิดที่ใช้แล้วไปดูที่ค่าของcageอยู่ในขอบเขตโดยรอบในเวลาที่คุณเรียกใช้ฟังก์ชัน นี่คือปัญหา เมื่อคุณเรียกใช้ฟังก์ชันของคุณฟังก์ชันนั้นget_pettersจะเสร็จสิ้นการคำนวณแล้วก็เป็นผลลัพธ์ cageตัวแปรท้องถิ่นในบางจุดในระหว่างการดำเนินการที่ได้รับมอบหมายให้แต่ละ'cow', 'dog'และ'cat'สตริง แต่ในตอนท้ายของการทำงานที่มีค่าสุดท้ายที่cage 'cat'ดังนั้นเมื่อคุณเรียกแต่ละฟังก์ชันที่ส่งคืนแบบไดนามิกคุณจะได้รับค่าที่'cat'พิมพ์ออกมา

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

  • ตัวอย่างฟังก์ชันบางส่วนโดยใช้functools.partial():

    from functools import partial
    
    def pet_function(cage=None):
        print "Mary pets the " + cage.animal + "."
    
    yield (animal, partial(gotimes, partial(pet_function, cage=cage)))
    
  • การสร้างตัวอย่างขอบเขตใหม่:

    def scoped_cage(cage=None):
        def pet_function():
            print "Mary pets the " + cage.animal + "."
        return pet_function
    
    yield (animal, partial(gotimes, scoped_cage(cage)))
    
  • การผูกตัวแปรเป็นค่าเริ่มต้นสำหรับพารามิเตอร์คำหลัก:

    def pet_function(cage=cage):
        print "Mary pets the " + cage.animal + "."
    
    yield (animal, partial(gotimes, pet_function))
    

ไม่จำเป็นต้องกำหนดscoped_cageฟังก์ชันในลูปการคอมไพล์จะเกิดขึ้นเพียงครั้งเดียวไม่ใช่ในการวนซ้ำแต่ละครั้ง


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

12

ความเข้าใจของฉันคือกรงถูกมองหาในเนมสเปซฟังก์ชันหลักเมื่อมีการเรียกใช้ pet_function ที่ให้ผลจริงไม่ใช่ก่อนหน้านี้

ดังนั้นเมื่อคุณทำ

funs = list(get_petters())

คุณสร้าง 3 ฟังก์ชันซึ่งจะพบกรงที่สร้างขึ้นล่าสุด

หากคุณแทนที่ลูปสุดท้ายด้วย:

for name, f in get_petters():
    print name + ":", 
    f()

คุณจะได้รับ:

cow: Mary pets the cow.
dog: Mary pets the dog.
cat: Mary pets the cat.

6

สิ่งนี้เกิดจากสิ่งต่อไปนี้

for i in range(2): 
    pass

print(i)  # prints 1

หลังจากทำซ้ำค่าของiจะถูกเก็บไว้อย่างเกียจคร้านเป็นค่าสุดท้าย

ในฐานะตัวสร้างฟังก์ชันจะทำงานได้ (เช่นพิมพ์แต่ละค่าตามลำดับ) แต่เมื่อเปลี่ยนเป็นรายการมันจะทำงานบนเครื่องกำเนิดไฟฟ้าดังนั้นการเรียกทั้งหมดไปที่cage( cage.animal) ส่งคืนแมว


0

มาทำให้คำถามง่ายขึ้น กำหนด:

def get_petters():
    for animal in ['cow', 'dog', 'cat']:
        def pet_function():
            return "Mary pets the " + animal + "."

        yield (animal, pet_function)

จากนั้นเช่นเดียวกับในคำถามเราได้รับ:

>>> for name, f in list(get_petters()):
...     print(name + ":", f())

cow: Mary pets the cat.
dog: Mary pets the cat.
cat: Mary pets the cat.

แต่ถ้าเราหลีกเลี่ยงการสร้างสิ่งlist()แรก:

>>> for name, f in get_petters():
...     print(name + ":", f())

cow: Mary pets the cow.
dog: Mary pets the dog.
cat: Mary pets the cat.

เกิดอะไรขึ้น? เหตุใดความแตกต่างที่ลึกซึ้งนี้จึงเปลี่ยนผลลัพธ์ของเราอย่างสิ้นเชิง


ถ้าเราดูlist(get_petters())มันชัดเจนจากที่อยู่หน่วยความจำที่เปลี่ยนไปซึ่งเราให้ฟังก์ชันที่แตกต่างกันสามอย่าง:

>>> list(get_petters())

[('cow', <function get_petters.<locals>.pet_function at 0x7ff2b988d790>),
 ('dog', <function get_petters.<locals>.pet_function at 0x7ff2c18f51f0>),
 ('cat', <function get_petters.<locals>.pet_function at 0x7ff2c14a9f70>)]

อย่างไรก็ตามลองดูที่cellฟังก์ชันเหล่านี้เชื่อมโยงกับ:

>>> for _, f in list(get_petters()):
...     print(f(), f.__closure__)

Mary pets the cat. (<cell at 0x7ff2c112a9d0: str object at 0x7ff2c3f437f0>,)
Mary pets the cat. (<cell at 0x7ff2c112a9d0: str object at 0x7ff2c3f437f0>,)
Mary pets the cat. (<cell at 0x7ff2c112a9d0: str object at 0x7ff2c3f437f0>,)

>>> for _, f in get_petters():
...     print(f(), f.__closure__)

Mary pets the cow. (<cell at 0x7ff2b86b5d00: str object at 0x7ff2c1a95670>,)
Mary pets the dog. (<cell at 0x7ff2b86b5d00: str object at 0x7ff2c1a952f0>,)
Mary pets the cat. (<cell at 0x7ff2b86b5d00: str object at 0x7ff2c3f437f0>,)

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

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

ในลูปที่สองระหว่างการวนซ้ำแต่ละครั้งเรากำลังหยุดget_petters()เครื่องกำเนิดไฟฟ้าชั่วคราวและเรียกใช้fหลังจากหยุดแต่ละครั้ง ดังนั้นเราจึงดึงค่าของanimalช่วงเวลานั้นในช่วงเวลาที่ฟังก์ชันเครื่องกำเนิดไฟฟ้าหยุดชั่วคราว

ดังที่ @Claudiu ให้คำตอบสำหรับคำถามที่คล้ายกัน :

มีการสร้างฟังก์ชันแยกกันสามฟังก์ชัน แต่แต่ละฟังก์ชันมีการปิดสภาพแวดล้อมที่กำหนดไว้ในกรณีนี้คือสภาวะแวดล้อมส่วนกลาง (หรือสภาพแวดล้อมของฟังก์ชันภายนอกหากวางลูปไว้ในฟังก์ชันอื่น) ตรงนี้เป็นปัญหาที่เกิดขึ้นแม้ว่า - ในสภาพแวดล้อมนี้กลายพันธุ์และการปิดทั้งหมดให้ดูที่เดียวกันanimalanimal

[หมายเหตุบรรณาธิการ: iถูกเปลี่ยนเป็นanimal.]

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