การสร้างฟังก์ชันแบบวนซ้ำ


107

ฉันกำลังพยายามสร้างฟังก์ชันภายในลูป:

functions = []

for i in range(3):
    def f():
        return i

    # alternatively: f = lambda: i

    functions.append(f)

ปัญหาคือฟังก์ชันทั้งหมดจบลงเหมือนกัน แทนที่จะส่งคืน 0, 1 และ 2 ฟังก์ชันทั้งสามจะคืนค่า 2:

print([f() for f in functions])
# expected output: [0, 1, 2]
# actual output:   [2, 2, 2]

เหตุใดจึงเกิดขึ้นและฉันควรทำอย่างไรเพื่อให้ได้ฟังก์ชันที่แตกต่างกัน 3 ฟังก์ชันที่เอาต์พุต 0, 1 และ 2 ตามลำดับ


4
เพื่อเป็นการเตือนตัวเอง: docs.python-guide.org/th/latest/writing/gotchas/…
Chuntao Lu

คำตอบ:


172

คุณกำลังประสบปัญหาเกี่ยวกับการเชื่อมต่อล่าช้า - แต่ละฟังก์ชันจะค้นหาiช้าที่สุดเท่าที่จะทำได้ (ดังนั้นเมื่อเรียกใช้หลังจากสิ้นสุดลูปiจะถูกตั้งค่าเป็น2)

แก้ไขได้อย่างง่ายดายโดยการบังคับให้ผูกในช่วงต้น: เปลี่ยนdef f():เป็นdef f(i=i):แบบนี้:

def f(i=i):
    return i

ค่าเริ่มต้น (ทางด้านขวาiในi=iเป็นค่าเริ่มต้นสำหรับชื่ออาร์กิวเมนต์iซึ่งเป็นด้านซ้ายมือiในi=i) จะมองมาที่defเวลาไม่ได้อยู่ที่callเวลาดังนั้นพวกเขากำลังหลักวิธีการมองหาเฉพาะที่มีผลผูกพันในช่วงต้น

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

def make_f(i):
    def f():
        return i
    return f

และในวงของคุณใช้f = make_f(i)แทนdefคำสั่ง


7
คุณรู้วิธีแก้ไขสิ่งเหล่านี้ได้อย่างไร
alwbtc

3
@alwbtc ส่วนใหญ่เป็นเพียงประสบการณ์คนส่วนใหญ่ต้องเผชิญกับสิ่งเหล่านี้ด้วยตัวเองในบางช่วงเวลา
ruohola

คุณช่วยอธิบายได้ไหมว่าทำไมมันถึงใช้งานได้ (คุณช่วยฉันด้วยการโทรกลับที่สร้างขึ้นในลูปอาร์กิวเมนต์เป็นช่วงเวลาสุดท้ายของลูปดังนั้นขอขอบคุณ!)
Vincent Bénet

23

คำอธิบาย

ปัญหาที่นี่เป็นที่ค่าของiไม่ได้ถูกบันทึกเมื่อฟังก์ชั่นfจะถูกสร้างขึ้น แต่fเงยหน้าขึ้นค่าของiเมื่อมันถูกเรียกว่า

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

global_var = 'foo'

def my_function():
    print(global_var)

global_var = 'bar'
my_function()

เมื่อคุณอ่านโค้ดนี้คุณต้องคาดหวังว่ามันจะพิมพ์ "bar" ไม่ใช่ "foo" เนื่องจากค่าของglobal_varมีการเปลี่ยนแปลงหลังจากที่มีการประกาศฟังก์ชัน สิ่งเดียวกันที่เกิดขึ้นในรหัสของคุณเอง: เมื่อถึงเวลาที่คุณโทรหาfค่าของการมีการเปลี่ยนแปลงและได้รับการตั้งค่าi2

การแก้ไขปัญหา

มีหลายวิธีในการแก้ปัญหานี้ นี่คือตัวเลือกบางส่วน:

  • บังคับใช้การเชื่อมโยงก่อนกำหนดiโดยใช้เป็นอาร์กิวเมนต์เริ่มต้น

    ซึ่งแตกต่างจากตัวแปรปิด (เช่นi) อาร์กิวเมนต์เริ่มต้นจะได้รับการประเมินทันทีเมื่อกำหนดฟังก์ชัน:

    for i in range(3):
        def f(i=i):  # <- right here is the important bit
            return i
    
        functions.append(f)
    

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

    >>> i = 0
    >>> def f(i=i):
    ...     pass
    >>> f.__defaults__  # this is where the current value of i is stored
    (0,)
    >>> # assigning a new value to i has no effect on the function's default arguments
    >>> i = 5
    >>> f.__defaults__
    (0,)
    
  • ใช้โรงงานฟังก์ชันเพื่อจับค่าปัจจุบันของiการปิด

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

    def f_factory(i):
        def f():
            return i  # i is now a *local* variable of f_factory and can't ever change
        return f
    
    for i in range(3):           
        f = f_factory(i)
        functions.append(f)
    
  • ใช้functools.partialเพื่อผูกค่าปัจจุบันของiถึงf

    functools.partialช่วยให้คุณสามารถแนบอาร์กิวเมนต์กับฟังก์ชันที่มีอยู่ ในทางหนึ่งมันก็เป็นโรงงานฟังก์ชันเช่นกัน

    import functools
    
    def f(i):
        return i
    
    for i in range(3):    
        f_with_i = functools.partial(f, i)  # important: use a different variable than "f"
        functions.append(f_with_i)
    

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

>>> i = []  # instead of an int, i is now a *mutable* object
>>> def f(i=i):
...     print('i =', i)
...
>>> i.append(5)  # instead of *assigning* a new value to i, we're *mutating* it
>>> f()
i = [5]

สังเกตว่าiยังคงเปลี่ยนแปลงอยู่แม้ว่าเราจะเปลี่ยนเป็นอาร์กิวเมนต์เริ่มต้นก็ตาม! หากรหัสของคุณกลายพันธุ์ iคุณจะต้องผูกสำเนาของiฟังก์ชันของคุณดังนี้:

  • def f(i=i.copy()):
  • f = f_factory(i.copy())
  • f_with_i = functools.partial(f, i.copy())
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.